Skip to content

Run tracing

One measurement primitive, a span, records every unit of work OpenSwitchboard does — from the moment a message reaches the process to the moment its first reply lands, every named background step, and every step on the Workers — and the per-run timeline falls out of it. Every instant of a run's window belongs to exactly one of seven terms, so a user can explain every number they see and no gap can hide. Decision record: spans are the one measurement primitive.

  • Code: src/core/trace/ (types, tracer, context, sinks, runStreamSink, cardSink, streamSpans, attrs, partition, classify, spanLog, traceparent, displayNames, clockReads, clockScan, clockAllowlist.json), src/core/normalizeSpans.ts (the stream Adapter: normalizeSpans, spansFromEvents, lossesFromStream), src/core/time/formatDuration.ts, src/core/testing/ (recordingSink, tickingClock), scripts/clock-allowlist.mts, the clock-ban rule in eslint.config.mjs, tracing in src/config.ts, src/core/runEnding.ts (how a dispatch ends its runs: finish when the agent stops, the seal after the first reply attempt) (validated in src/config/validate.ts)
  • Docs: reference: configuration (tracing.log); How a request flows, Worker topology
  • Tests: src/core/trace/*.test.ts, src/core/normalizeSpans.test.ts, src/core/time/formatDuration.test.ts

Behavior

  1. One primitive. A span has a start, an end, a name and a parent; a root has no parent, one per message. Every awaited step runs inside span(fn) (Execute Around Method): fn is invoked synchronously and the span ends when fn settles, ok on return, error and rethrow on throw. A handle (start/end) is a span kept across a suspension point; end() is idempotent; startedAt may backdate a span created after the fact; a child started after its parent ended is a late child, recorded with true times and never streamed. Sinks (Observer) are attached to a root and inherited by its subtree; a throwing sink never reaches traced code. Names are sanitized to [a-z0-9_.-], 64 chars, with a stable hash suffix for a cut name.
  2. Errors carry a classification, not a body. classifyError(err, { kind, code }) marks an error at the point that knows the peer's discriminator (an HTTP status, the resident's needs); fail(err) at any depth (through cause, to depth 5) records errorKind/errorCode and no message. An unclassified error records its message redacted and capped at 200. A span whose error originates in a remote body never carries free text.
  3. The log sink. One JSON line per span end: span, traceId, spanId, parentSpanId, startedAt, ms, status, errorKind/errorCode/errorMessage when present, attrs — never text, summary or output. tracing.log: roots (default; one or two lines per run) prints roots only — a process's own roots, which includes a Worker's root that adopted the bot's trace context (SpanRecord.adopted, item 22; the line still shows the remote parentSpanId), never a child; slow adds every span of 1 s or more. Attributes are a closed key union with per-key domains; the string-valued keys are sanitized identifiers from closed tables, never free text.
  4. Which spans reach a run's stream, and how each counts. An enumerated set plus four prefix families (tool., mcp., dispatch.workspace.attach., run.command.) stream; everything else is log-only. Each streamed name is counted (into one of four buckets: getting ready, thinking, in tools, finishing up), uncounted (structure only: request, run.agent, ship.round, run.settle_reviewed_head, post.*), or background (run.reading_diff, ignored by the claim pass). run.command and its grafts count as tools on a command run and as getting ready on an agent run. Invariant: no counted name is reachable under a background parent, and every ancestor of a counted name in a different bucket is uncounted, under both owners.
  5. The partition. window = getting ready + thinking + tools + finishing up + Switchboard overhead + not recorded + not loaded, by construction. The claim pass gives each instant to the deepest counted span covering it (ties by earlier start, then id), so concurrent siblings count once. An open span runs to the window end on a live window; on a finished window a counted open span is cut at the first loss interval after its start. lost intervals (registry head trim, seq gaps, spans_dropped) are not recorded; elided ones (a live replay's budget) are not loaded; lost wins where they overlap; a counted claim beats both. Overhead is the residual. The printed shape floors every term to whole seconds and lets the residual absorb the rounding, so the printed items always sum to the printed total and no term prints 0s; the shape is shown when at least two terms are informative (5 % of the window or 2 s).
  6. The run-stream sink. Attached to the root before any run exists, it retains every streamed start and end in one bounded append-only buffer (512 events or 256 KiB; the first 64 never dropped; drops from the middle keep the newest). bindRun(runId, publish) backfills everything retained as fresh objects, publishes one counted spans_dropped note when something was dropped, then routes live. A rebind after a run ended is normal (the natural-language fall-through); a rebind over an active binding releases it with one warning; anything after the root ended is dropped with one warning while the log sink keeps it. Two roots never cross.
  7. The card sink. Maps each streamed dispatch.* start to a display label and clears it when run.agent starts; a Null Object until a card is bound.
  8. The clock ratchet. Production code reads the wall clock only through the injected clock(); src/core/trace/clock.ts is the one file that touches Date. ESLint's clock-ban rule and the allowlist scanner share one predicate list (Date.now, globalThis.Date.now, Date['now'], zero-argument new Date(), performance.now, process.hrtime, process.hrtime.bigint, process.uptime; Date.parse/Date.UTC are not reads). The allowlist (clockAllowlist.json) is empty: every production read goes through clock.ts — the bot through its injected clock or systemClock, the Workers and the scripts through systemClock, the web through web/src/lib/wallClock.ts (wallNow, and useWallClock for the pages that tick) — and the ESLint override that once exempted listed files is gone, so clock-ban applies everywhere the ratchet does. npm run clock:check keeps the list at {}; a regeneration that is not empty names a new direct read. Tests, test helpers, the two clocks and the deploy tooling are exempt.
  9. One duration formatter. formatDuration(ms, style): precise (800ms, 1.3s, 5m 04s) for a single measured step, clock (0s, 38s, 4m 12s, 1h 03m; non-finite or negative reads 0s, missing reads empty) for a ticking stopwatch, report (850ms, 45s, 1m 18s, 1h 05m) for a compact total. It replaced the three exported formatters and the run timeline's inline copy.
  10. Trace context between our Workers. Strict W3C traceparent (version 00, lowercase, no all-zero id). The container's edges ignore an inbound header and mint their own; internal Workers adopt one only inside their authenticated branch; the header is set only toward our own hosts. A traceId is not a capability and is never echoed to a caller.
  11. Note kinds are one list. RUN_NOTE_KINDS is derived from the RunNoteKind union (a new kind is a type error until listed), and friction analyze filters notes by it, so every kind survives analysis the day it is added. spans_dropped is the kind the run-stream sink publishes.
  12. The degraded self-hosted shape. With no Workers there are no Worker roots, no grafted resident steps and no cross-Worker trace context; backend is local; the span log is one or two lines per run under roots; a stdin capture for friction analyze carries no stamps and prints no shape.
  13. One duration, six surfaces, and readers that tolerate the future. runDurationMs(stamps, now?) (src/core/runDuration.ts) is the one definition: the window opens at receivedAt (our process saw the message; absent until the adapters carry it, so every reader falls back to the registry's startedAt) and closes at finishedAt, or at the caller's now while live; a finished run without finishedAt has no duration and renders blank. runs list, the runs index row, the run page header (createRunClock: the seed's serverNow projected forward by the browser time since the seed arrived, so a live tick never subtracts a server stamp from the browser's clock), the history seed and, later, the Slack card and the friction runMs all call it, so no two surfaces can disagree. The stamps (receivedAt, sealedAt, replyOk, stepCount, schema) ride RunMeta, RunSummary, RunSnapshot, RunView, the seeds and RunRecord as optional, omitted-when-absent fields, validated wherever a runtime validator exists, clamped by the Worker like finishedAt, and preserved across an index repaint. Readers are tolerant before the emitters exist: span records never count as events or move a stream's first/last stamps (the analyzer, the page model), and a stream payload with no string type is a transport frame, never garbage (the capture parser).
  14. Span records on the stream, and the one Adapter that reads them. span_start / span_end are two RunEvent variants (src/core/runEvents.ts; run-visibility.md item 1): published, counted and stored like every other event, invisible to every content reader (isSpanRecord). normalizeSpans(events) (src/core/normalizeSpans.ts) is the one Adapter from a stream to its span set, and spans are the only timing record: a stamped tool_call/tool_result pair keyed by callId whose tool.* twin is missing (dropped by the record budget) gets one back (synth:<callId> for a plain token, else synth:<index>), placed around the pair, open when the call is unpaired, parented to the innermost surviving run.agent span containing it; a call with no callId or no at gets nothing, a result with no callId closes nothing, and no span is ever invented for a model turn. It never invents a span for content that has one, so it is idempotent and the identity on a complete stream, and it never mutates its input. A record whose schema is absent or below SPAN_SCHEMA (2) predates spans and is never normalized: the run page hands its events over as stored with RunHistorySeed.untimed and the timeline states no timing data; every other reader sees a content stream with no spans (migrations.md). spansFromEvents(events, traceId) pairs the records into SpanRecords (an end alone is complete; a start alone is open); lossesFromStream(events, { windowStart, elided }) derives the loss intervals — a trimmed head (lost), each interior seq gap (elided inside a range the live transport reported, lost otherwise), every spans_dropped note's own range (lost); replay_note markers are never consulted.
  15. Display names. DISPLAY_NAMES (src/core/trace/displayNames.ts) names every enumerated streamed span (satisfies Record<StreamedSpanName, string>; unique by test), and displayNameOf(name) covers the prefix families — a tool's own name, an MCP tool's own name, the resident step's label (RESIDENT_STEP_LABELS in src/execution/residentSteps.ts, the one table both sides share: the Worker's runners take a ResidentStepName, so a step the table lacks is a type error in the Worker, and a source-scan test keeps the table free of labels for steps the Worker no longer names) — with a Switchboard step for an unknown leaf. No user surface prints a raw span name: the run page's span rows and, later, the card's setup label and the timeline's ranked list all read this table.
  16. The protected head and the content count. The registry's backlog (live-view.md item 2) keeps the events that say what a run is — input, context, run_meta, the root's start, the slack.receive and dispatch.* span pairs, the mcp_unavailable/spans_dropped/cold_sandbox notes — as a protected head of at most HEAD_BUDGET_BYTES (512 KiB), capped per event to MAX_EVENT_BYTES at publish, never trimmed by the count (8000) or byte bound; a fresh subscribe replays the head first. The record budget (run-history.md item 6) drops span pairs first, from the middle outward, never from the head, so spans displace no content. stepCount — content events, span records excluded — rides the summary, the snapshot, the record and the index row's count cell beside eventCount.
  17. The emitters — the runner, the MCP bridge, the executor, the review settle, the ship pipeline, the dispatcher. runAgent opens run.agent under RunOptions.span and, under it, one model.turn per provider call (live-view.md item 15) and one tool.<name> span(fn) per tool call: the call is announced inside it, so its tool_call/tool_result — and whatever the tool publishes, a skill_use — carry its spanId; the call's ToolContext carries the span and a TracingExecutor (execution.md item 15), and the span ends with callId, ok, exitCode?, infra? (error on a failed or unknown tool; a throw ends it through fail). The MCP bridge runs each remote call as mcp.<server>.<tool> under that span (mcp-tools.md item 10). settleReviewedHead is one uncounted run.settle_reviewed_head span whose re-review run.agent is its child; the ship pipeline runs every round inside ship.round (index, agent) with the child run's run.agent under it (agent-ship.md item 12). The dispatcher binds each run to the request's root (item 18): the root's span_start and the setup spans so far backfill as the stream's first events (head material), the loop's spans follow live, and the root ends after the seal, so its end never reaches a sealing registry and every record shows the request as its one open span. Without a parent span (the CLI, tests) the runner emits no span record and its stream is byte-identical. Every record carries schema: 2; the 💭 thought for … card line is the runner's onProgress note at each turn's end; there is no turn or mcp_tool_use event kind — the model.turn and mcp.* spans are the record (live-view.md item 13). The github.token_mint/github.rest spans wait for tracedFetch (the cross-Worker PR); GithubApiError and the resident's ExecInfraErrors are classified now (item 2; execution.md item 9), so the spans above them carry kind and code.
  18. One request, one root — the dispatcher's spans. startRequestRoot(deps, { channel, receivedAt }) (src/core/requestTrace.ts) is the one constructor of roots: each channel adapter calls it when our process sees the message — Slack before its redelivery guard, HTTP and MCP once the caller's identity is established and the body parsed, the CLI at entry — stamps the message receivedAt (Slack also originAt, the platform's ts) and hands the RequestTrace to dispatch() through DispatchOptions.trace (the DispatchFn seam carries it); a dispatch without one (tests, an older caller) starts its own at entry. The root's sinks are the log sink at tracing.log (or CoreDeps.sinks, a test's recording sink), the run-stream sink, the card sink and a collector for the runless closes; CoreDeps.clock and CoreDeps.tracer are the injection points the no-gaps test uses. Every awaited step of dispatch() is a span(fn) child of the root at the site the work starts: dispatch.history, dispatch.repo_context and dispatch.memory_read (wrapping the promises as they are created, so their overlap is real), dispatch.ack_card, dispatch.admission (the steer ack, a superseded resume), dispatch.refuse (every refusal — the close and the reply as one span, outcome naming why: agent_allowlist, repo_not_onboarded, repo_unverified, repo_access, pr_head_unknown, which_branch, branch_moved, ship_preflight, setup_failed…), dispatch.workspace.attach (backend), dispatch.gate.attached_head (outcome), dispatch.mcp_discovery, dispatch.compose (the wait on the memory read; the composition is synchronous), dispatch.channel_visibility, dispatch.ledger_claim, dispatch.ship_preflight; run.command (a command run's body, command naming it; a runless command reply too, log-only there); the background span run.reading_diff (outcome; started by startReviewReadingDiff under the root, so the diff's exec — and its http.client hop to a sandbox — is its child rather than a stray root on the Worker); run.observe_workspace, run.description_turn (the coding run's extra model turn for a missing PR description, pr-description.md item 5 — its run.agent hangs under it, as the re-review's does under run.settle_reviewed_head), run.pr_post_step, run.reading_diff_join, run.pr_description_join (the join on the PR description's store lookup, reading-diff.md item 7); post.card_close and post.reply (between finish and seal, on the stream); log-only post.workspace_release, post.ledger_finishing, post.review_post, post.followups, post.history_write, post.settled_outcome (a late child, minutes after). Sync decisions (the repo and PR-head gates) are attrs on the refusal that follows, not spans of their own. registry.create carries receivedAt (a resume keeps its original stamps); trace.bindRun follows it; run_meta carries traceId, and a command run publishes run_meta { agent: "command", traceId } with no model and no repo. The card ticks from receipt: its clock is receivedAt, a 5 s setup heartbeat repaints it through setup with the card sink's label (◐ *coding* on \m` · 42s — attaching the workspace…) until the run loop's own heartbeat takes over; every close carries the request's elapsed time (📦 … · not started (repo access) · 12s, ❌ setup failed · … · 3m 04s), and the shape line and the queued caption lead its detail when the card's gate passes (cardShapeLine: a minute of window or 15 s of getting ready, then item 5's informativeness rule; queuedCaptionfrom a minute) — a runless close over the root's children so far to now, a done close over[receivedAt, finishedAt]. The root ends in dispatch()'s outermost finally with status (completed, refusedwhen adispatch.refusehappened,stopped, failed), after the drain and the tail and before the fresh turn, which is a request of its own: received now, queuedBehindMs = now − the earliest follow-up's arrival on its root and its card. **The no-gaps test** (src/core/dispatcher.test.ts::no gaps…): an AsyncLocalStorageSpanContexton the injected tracer, a ticking clock that advances only when a timed fake settles, every awaited fake wrapped; on each golden path every tick has a span (anullspan is a gap) and the partition over the request's streamed spans equals the ticks per bucket withoverheadMs === the uncounted ticks + backgroundOnlyMs (0 on these paths). A done close reads its shape from the finish-site diagnosis (diagnosis.shape, [run-friction.md](run-friction.md) item 3) — the same partition the record stores and the report prints — while a runless close still partitions the root's children so far. Deferred to their own PRs: dispatch.gate.repo/dispatch.gate.pr_headas spans (sync today),post.reflection` (the scheduler returns nothing awaitable).
  19. The resident's and the sandbox's own measurements. The resident Worker records every command it runs for one /attach or /oprunOk's steps (clone, install, worktree-clean…), the op's command (test, build), and each wait for the mirror lock (mutex_wait, waitedMs) — into a per-request collector (src/execution/residentStepTrace.ts, scoped by an AsyncLocalStorage so a concurrent request's steps never land on the wrong answer) and returns them in the answer's trace: offsets from the request's start (startMs, durationMs), a status, exitCode/timedOut when the step was a command, bounded to 64 steps and 8 KB, names sanitized to [a-z0-9_-]. The bot re-validates at the parse boundary (sanitizeGraftedSteps, src/execution/residentTrace.ts — an Anti-Corruption Layer: every field rebuilt from an allowlist, error text dropped, an out-of-range exit code dropped, the bounds re-applied) and grafts the steps under the span that made the call (Span.graft, a child with both stamps supplied): an attach's under dispatch.workspace.attach as dispatch.workspace.attach.<step>, an op's under the command run's run.command as run.command.<step> (the trace rides the op's value and the chat result), each rebased so the resident's request start is the calling span's start and clipped to the bot's now, with backend: resident, exitCode, timedOut, waitedMs as attrs and an infra classification (never a message) on a failed step; the parent gets clockSkewMs — what the bot waited minus what the resident measured. A refusal carries the steps that led to it too (a failed attach's trace is the one that says which step blew the budget): the bot pins them on the error the refusal becomes (withResidentTrace/residentTraceOf, found through a wrapping cause), the factory's sandbox fallback hands them on as the selection's trace, and the dispatcher grafts them under the attach span whether the attach returned or threw. A Worker predating the trace binds and answers as before (no trace, no grafts). The sandbox Worker stamps durationMs on its /exec document — the sandbox's own wall time for the command, the serverMs a later PR puts on the tool.* span; nothing on the bot reads it yet, and the executor is indifferent to its presence. The resident's own roots (resident.attach, resident.op, the refresh cycle) and the sandbox's sandbox.exec log lines are the Worker-roots PR's.
  20. The bot's own roots, and the deploy's. Work no request caused gets a root of its own through startProcessRoot (src/core/requestTrace.ts), on the leading sinks only — it streams to no run and paints no card, so under tracing.log: roots it is one JSON line when it ends: slack.catch_up around each reconnect catch-up pass (slack-channel.md item 7; channels, missed, orphans, skipped), drain around the shutdown drain (signal, runs held at the start, handed to the next generation, sealed, abandonedRuns at the exit). Two more roots exist for the bot's calls to the resident Worker's admin listing, which would otherwise reach the Worker with no trace to adopt and mint one root per call there: resident.fleet_refresh around each background read of the fleet facts (routing-and-config.md item 11; httpStatus, residents), and dashboard.residents around each residents page request (route index/detail, httpStatus). Both hand the root to the admin client (withSpan), so the Worker's resident.fetch for /residents is a child on the bot's trace. The deploy runner (release-and-deploy.md item 19) starts one deploy.step.<worker> root per step on its own output at slow, outcome one of live/deployed/not_live/failed/threw (busy went with the deploy gate — release-and-deploy item 13), and its live gate is the step's one child, deploy.wait_live, whose waitedMs is the number the runner's live (…; Ns after the upload) line prints — the same value, never a second measurement. All of these are log-only names: they have no partition class and never stream. The clock ratchet (item 4) shrinks with them: the dispatcher, the composition root, the Slack adapter, the catch-up, the admission, the command registry and the deploy runner read no Date.now — the dispatcher through CoreDeps.clock, the runner through its deps.now, the rest through systemClock or a caller's now. The Workers' own roots (<worker>.fetch, cron.<schedule>) and the remaining reads are the later PRs'.
  21. Trace context between our own Workers — the bot's side. tracedFetch(parent, url, init, { route }) (src/core/trace/tracedFetch.ts) is the one way the bot calls a Worker: one log-only http.client span under the caller's span, ending at the response headers (the body is the caller's to read under its own deadline), carrying host, the caller's closed-table route, method and httpStatus — never a header, a query string or a body; a transport failure is classified (timeout on an abort, else transport) and carries no message. The traceparent header (00-<traceId>-<spanId>-01) is set only when the request's host is one of ours: internalHostsOf (src/core/trace/internalHosts.ts) computes the set once at startup from the configured URLs — the resident, the sandbox, the state Worker, the schedules and overrides Workers, the public shim — matched exactly on hostname[:port], never a suffix, and one [trace] internal hosts log line names it; a call to any other host (GitHub, Slack, a model provider, an MCP server) carries no trace context. Without a parent span there is no trace to carry and the call is a plain fetch. The parent reaches the clients explicitly, never through ambient state: the runner's TracingExecutor hands each exec.* span to the inner executor as opts.span (ExecTraceOptions on every Executor method), the resident and sandbox clients thread it to their one fetch, the dispatcher's dispatch.workspace.attach span reaches the factory's probe and attach, and Operations.run takes a trace.span. The container edges — the Slack, HTTP and MCP adapters and the live-view router — ignore an inbound traceparent/tracestate/baggage unconditionally: they mint their own root and have no code path that reads one. ScheduleFiring.traceId is the shim's trace for a firing, optional until the shim has roots. The shim's strip-and-mint, the Workers' own roots and log sinks, the cron roots and the github.* spans are the next PR's.
  22. Trace context between our own Workers — the Workers' side. The shared helpers are src/core/trace/workerTrace.ts. The public edge (the bot shim, deploy/cloudflare/worker.ts) strips every inbound traceparent/tracestate/baggage and never adopts one: each routed request is a bot-shim.fetch root whose route is a word from a closed table (shimRoute: healthz, ingress, mcp, runs, residents, costs, api, admin, docs, page, other), the forwarded request carries that root's traceparent, and a static asset, the favicon or the live view's SSE stream gets no root at all; each fired run schedule is a cron.<schedule> root whose traceparent rides the /ingress POST and whose trace id the recorded firing carries (ScheduleFiring.traceId). An internal Worker adopts the bot's traceparent only inside its authenticated branch (RootOptions.parent; startAdoptedRoot): the state Worker roots every authenticated, routed request as state.fetch with the route as attr (unknown paths, refusals and /healthz get none); the resident roots its three streamed routes inside the Durable Object where the work is — resident.attach and resident.op at the request's start with the collector's steps grafted as resident.<step> children and the one outcome word (ok, needs_ref, needs_attach, error), resident.exec with the exit code — and every other authenticated route as resident.fetch at the edge; the sandbox roots each command as sandbox.exec at the attempt that answered, with exitCode/timedOut, or classified infra with no message when the sandbox never answered. Every Worker logs at slow through workerLogSink, whose filter drops a root that ended 401 or 403, so an unauthenticated refusal produces no line on any edge. The github.* spans, the resident admin client and the state-Worker stores stay for the next PR.
  23. The GitHub client's spans. The REST client (RestGithubApi, src/execution/githubApi.ts) is span-aware through a view: withSpan(span) returns the same client — the same fetch, the same token resolver and its module-wide cache — bound to one span, and the runner binds one per tool call (ToolContext.github.api is the view under the call's tool.<name> span; a client without withSpan, a test double, is passed as is). Every request the view makes is one log-only github.rest span under the call: host (api.github.com), route from the closed GithubRoute table (installation_repos, contents, contents_raw, search_code, issues, issue, issue_comments, issue_create, issue_update, issue_comment_create, graphql, app_installation_token) — never the path, which carries a repo, a file path or a query — method and httpStatus; a non-2xx ends the span ok with its status and the caller throws the classified GithubApiError as before. The App token mint (resolveGithubToken(scope, span), src/execution/githubApp.ts) is a github.token_mint child of the caller's span with scope, cached (the module cache answered) and expiresInMs, its access_tokens POST a github.rest child under it; a cache hit is a mint span with cached: true and no request. GitHub is never an internal host, so no traceparent leaves with any of these. tracedFetch takes the span's name (http.client by default, github.rest for this client). The shared client the dispatcher builds once stays unbound: the post steps, the review settle, the ship pipeline and the githubComments/githubPulls/githubIssues helpers call GitHub without a span today and are unmeasured.
  24. The bot's other clients under a span. Three more clients that talk to our own Workers from the bot join the trace the way the resident and sandbox clients do (item 21), each through tracedFetch with the route as the path literal from a closed table, never a query. The resident admin client (makeResidentAdminClient, src/core/residentAdmin.ts) through a withSpan(span) view: the repo.* commands bind it to the command's span, which the registry hands every handler as CommandContext.span (CommandRegistry.invoke(id, input, caller, deps, { span }), forwarded by bindCommands and by invokeChatCommand's span argument; the dispatcher passes its run.command span), so a repo rebuild's /rebuild call is an http.client child of the command's step and the /status?resource=… poll carries route: "/status" and never the resource. The memory Worker store (WorkerMemoryStore.retrieve(q, { span })) from memoryContextBlock's span argument, the dispatcher's dispatch.memory_read, so every scope's /retrieve is a child of the read. The run Worker store (WorkerRunStore.put(record, { span })) from the history writer's write(record, { span }), which the dispatcher gives the request's root at each of its three record registrations; the record is written after the reply, so its /runs/put is usually a late child of the root — recorded with its true times, log-only. Without a span each client is the plain fetch it was, and a client double without withSpan is used as is. Still unmeasured, stated: the admin status poll the onboard/rebuild settle makes (a settle has no span), the memory store's write, list and forget, the run store's reads, the interrupted-run records the drain writes, the ledger's finish sink, and the schedule and friction-ledger clients.
  25. The Workers' own roots for work no request caused. Like the bot's process roots (item 20), each Worker roots the work its own timers start, through the same startAdoptedRoot with no context to adopt, logged by the Worker's sink. The resident Worker's cron firing is one resident.watchdog root (outcome = the firing's, residents = how many were checked; each resident's watchdogCheck a resident.check child ending with the action taken as outcome — never the resource, which names a repo), and the firing the state Worker records carries the root's trace id like the shim's cron roots do; the /debug run-watchdog op runs the same pass bare. The resident's self-rescheduling freshness cycle (onRefreshAlarm) is one resident.refresh root per cycle with every command it ran as a resident.<step> child, the way an attach's steps hang under resident.attach (item 19) — a cycle that parks, restarts the container or stops early still ends its root, error when it threw. The state Worker's retention sweep (RunHistoryDO.alarm) is one state.alarm root ending with swept, the rows it deleted. Every one of these prints as a root line at either log level; a Worker's console is the only place they are observable, so their receipts are live (wrangler tail).
  26. The bot's span log, readable by us and never from the outside. The container's stdout is not readable from outside Cloudflare, and tracing.log prints only roots (or roots and slow spans) there anyway — so the bot keeps its own span log in the process: createSpanLog (src/core/trace/spanLog.ts) is a sink every root this process starts also feeds (CoreDeps.spanLog, joining the leading sinks beside the log sink whenever a test has not injected sinks), holding every span end at every level as the same line the log sink prints plus endedAt, in a ring bounded by lines (20 000) and bytes (8 MiB) that lets the oldest go first and counts what it dropped. GET /admin/trace/log (src/channels/adminTraceLog.ts) answers an ingress bearer whose actor holds trace:read — the same token map and grant rule as the restart and the crash (authorizeIngressBearer), 401 without a bearer, 403 without the grant, 503 without the map, 405 for anything but GET — with { lines, matched, kept, dropped, oldestAt } filtered by since (epoch ms, on endedAt), traceId, span (a name or the family under it: github matches github.rest) and limit (500 by default, 5 000 at most, the newest); a malformed filter is a 400, never "everything". The shim forwards /admin/* to the container untouched and the Access gate does not cover it, so the bearer is the whole door. A line here carries what a log line carries and nothing more: never text, a body, a header or a credential.

Validation criteria

CriterionProof
1: span(fn) invokes fn synchronously, ends ok with the measured duration and nests under its parent; a throwing fn ends the span as error and rethrows; end() is idempotent; startedAt backdates; a late child is recorded with true times; a throwing sink never reaches traced code; records are copies; the test context propagates the current span through awaits and a bare await sees none[unit] src/core/trace/tracer.test.ts::createTracer::*
1: names are sanitized and a cut name carries a stable hash suffix[unit] src/core/trace/tracer.test.ts::sanitizeSpanName::*
2: a classified error records kind and code and no message at any depth through cause; an unclassified one records a redacted, capped message; a code is a short identifier, never prose; an HTTP status code is an integer in 100..599[unit] src/core/trace/tracer.test.ts::createTracer::a classified error records kind and code and no message, at any depth through \cause`; src/core/trace/classify.test.ts::classifyError / classificationOf::*`
3: roots prints the root only, slow adds spans of 1 s or more; a root that adopted a remote parent prints as a root at both levels with its remote parentSpanId and no adopted field on the line; the line carries exactly the documented fields and never text, summary, output or a credential; the null sink observes nothing[unit] src/core/trace/sinks.test.ts::createLogSink::*, src/core/trace/tracer.test.ts::createTracer::a root started with a remote parent carries that trace id and parent span id; without one it mints its own
3, 4: attrs validate domains and identifier shapes; every streamed name and prefix has a class under both owners; log-only names have none; run.command flips bucket with the owner; the ancestor invariant holds; PARENTS covers the streamed set exactly[unit] src/core/trace/streamSpans.test.ts::streamSpans::*, ::attrs::*
5: the identity holds on the worked example (4m 12s = 32 s · 2m 30s · 55 s · 8 s · 7 s), concurrent siblings, an MCP call inside a tool, a head-moved re-review, a command run and the fall-through, an open root and an open background upgrade, live vs finished open spans, lost vs elided losses with clipping and precedence, head and middle truncation, spans outside the window; the printed shape floors and sums; the informativeness gate[unit] src/core/trace/partition.test.ts::partition::*
6: retain, backfill as fresh objects, route live; one root two runs both see the setup with no cross-delivery; two roots never cross; late children and post-root spans never stream with one warning; the bounded buffer keeps its head and announces the drop with one note; a rebind over an active binding warns once[unit] src/core/trace/runStreamSink.test.ts::createRunStreamSink::*; on a real dispatch the setup spans lead the stream before input and the post spans trail the answer — src/core/dispatcher.test.ts::live run-view wiring (Area 2)::registers the run, publishes its events, and finishes it; a command run rides the same root — ::inline command runs + run receipts …::\friction report` is a run…`
7: the card sink paints setup labels and clears them at run.agent, nothing before a card is bound[unit] src/core/trace/runStreamSink.test.ts::createCardSink::*, src/core/requestTrace.test.ts::startRequestRoot::a bound card is told the setup step…; on the Slack card the setup heartbeat paints — attaching the workspace… with the elapsed time and the run's frames drop it — src/core/dispatcher.test.ts::executor provisioning by agent resources::a slow attach shows on the card…
8: the scanner and the ESLint rule name the same reads; every read shape is counted and Date.parse/Date.UTC/new Date(x) are not; .vue script blocks are scanned; tests and the one clock are exempt; the allowlist equals the tree exactly and the problem report names both directions[unit] src/core/trace/clockAllowlist.test.ts::clock ratchet::*
9: the three styles on their edge cases[unit] src/core/time/formatDuration.test.ts::formatDuration::*
10: the W3C vector parses with its flag; other versions, uppercase, wrong lengths, all-zero ids and non-strings are refused; format round-trips[unit] src/core/trace/traceparent.test.ts::parseTraceparent::*
3, config: tracing.log accepts roots and slow and refuses anything else at load[unit] src/config.test.ts::tracing::*
11: the note-kind list is derived from the union[unit] src/core/runEvents.test.ts::RUN_NOTE_KINDS::*
13: runDurationMs — received (or started) → finished, live to the caller's now, a tombstone has none, skew never reads negative; runs list, the index row, the history seed and the run page header all call it[unit] src/core/runDuration.test.ts::runDurationMs::*, src/core/commandRegistry.test.ts::*::renderCompact renders runs.list as one line per run with short id, agent, status, duration only, web/src/lib/indexRow.test.ts::stopwatch::*, web/src/lib/runPageModel.test.ts::header stopwatch (item 22)::*
14: normalizeSpans — a stamped, callId-keyed tool pair becomes a tool.* span placed around the pair (open when unpaired; the index when the id is not a plain token); a call with no callId or no stamp, and a result with no callId, get nothing; no span is invented for a model turn; the parent the innermost surviving run.agent; idempotent, the identity on a twinned stream, never mutating; spansFromEvents pairs starts and ends; lossesFromStream yields lost/elided intervals and ignores replay_note[unit] src/core/normalizeSpans.test.ts::normalizeSpans — the twin rule::*, src/core/normalizeSpans.test.ts::normalizeSpans — idempotence and the identity::*, src/core/normalizeSpans.test.ts::spansFromEvents::*, src/core/normalizeSpans.test.ts::lossesFromStream::*
15: every enumerated streamed span has a unique display name, none is a raw name, and every prefix family has a rule with the generic fallback[unit] src/core/trace/displayNames.test.ts::display names::*
15: every step name the resident Worker passes to a command runner has a label under both graft prefixes, no label is a raw name, and no label names a step the Worker no longer runs; the Worker's runners are typed against the table's keys[unit] src/core/trace/displayNames.test.ts::resident step labels (docs/reference/specs/tracing.md item 15)::*; [type] deploy/cloudflare-resident/worker.ts compiles only with every runner step in RESIDENT_STEP_LABELS
16: the protected head (registry) and spans-first record fit — see live-view.md and run-history.md rows; stepCount on the summary, snapshot and count cell[unit] src/core/runRegistry/backlog.test.ts::RunRegistry — backlog bounds::the protected head::stepCount counts content events only…, web/src/lib/indexRow.test.ts::…::the count cell prints the content-event count…
13: reader tolerance — span records and transport payloads are invisible to the analyzer's counts and stream clock, to the page model's stamps, and to the capture parser's skip count[unit] src/core/runFriction.test.ts::analyzeRunFriction — span records are invisible to counts and to the stream clock::*, src/core/runEventLines.test.ts::*::a payload with no type field is a transport frame, never garbage; an event-shaped payload this reader does not know is skipped and counted (docs/reference/specs/tracing.md), web/src/lib/runPageModel.test.ts::header stopwatch (item 22)::span records never move the runner clock or the stream's first/last stamps; no stamped events yet → no clock
17: the runner — run.agent wraps the loop; model.turn ends before what it produced with model, stop reason and usage; tool.<name> wraps the call, the pair and the tool's own publishes carry its id, its end carries the outcome (infra on an infra failure, error on an unknown tool); exec.* are log-only under it with the backend; no parent → no spans[unit] src/runner.test.ts::model turn and tool spans (docs/reference/specs/tracing.md)::*, src/execution/tracingExecutor.test.ts::TracingExecutor::*
17: the dispatcher — the root's start opens the stream before input and the loop's spans, the answer and the root's late end follow on one stream; the MCP call is an mcp.* span under the tool span; every model.turn lies inside a ship round; the record carries schema 2[unit] src/core/dispatcher.test.ts::live run-view wiring (Area 2)::registers the run, publishes its events, and finishes it, ::run history write path …::a completed run ends as one stored record…, ::run history write path …::tombstone-first provisional records …::a provisional interrupted record is written at run start…, ::MCP tools …::a general run gets the bridged tools + the MCP block…, ::agent:ship (pipeline)::round events…
17: RunEnding.finished(id, { afterSeal }) runs the hook once after that run's seal, before the writers, and logs a throwing hook[unit] src/core/runEnding.test.ts::createRunEnding — seal after the reply, records after the seal::finished(id, { afterSeal })…
17: the analyzer takes model time from model.turn span ends only — never from the result→call gap[unit] src/core/runFriction.test.ts::analyzeRunFriction — span records are invisible to counts and to the stream clock::model.turn span ends are the model time…
17: the history seed of a span-schema record is normalized before the markers — a pair whose twin the budget dropped gains it, markers count and place by seq only; a record below SPAN_SCHEMA is handed over as stored and flagged untimed[unit] src/channels/liveView.test.ts::live view on RunsService: history pages + index toggle …::persisted run page (history mode)::200s tokenless…, ::persisted run page (history mode)::a span-schema record's seed is the stream normalized…, ::AE11: truncated records::the persisted page seeds the marker in place…
17: the MCP bridge under a span, the classified GitHub and resident errors[unit] src/mcp/bridge.test.ts::bridgeMcpTools…::under a tool span…; the classification points are exercised by every existing GithubApiError/ExecInfraError test (the mark is a side table, never a message change)
17: the reading diff's two background spans are started by startReviewReadingDiff under the root, each diff's exec their child, and produceReadingDiff hands its span to the executor[unit] src/core/readingDiff.test.ts::reading diff spans (docs/reference/specs/tracing.md items 17/18)::*
17: the review settle's head probe and worktree move carry run.settle_reviewed_head to the executor, the workspace release carries post.workspace_release, the coding post step's workspace probes carry run.observe_workspace (the ship round's span inside a ship), the description turn's agent loop hangs under run.description_turn (under the request root for a coding run, under ship.round inside a ship), and the ship pipeline's releases carry ship.round[unit] src/core/codingPrPostStep.test.ts::observeCodingWorkspace::every probe carries the caller's span…, src/core/descriptionTurn.test.ts::runDescriptionTurn — one clipped turn on the run's own messages::publishes the description_turn note…, [unit] src/core/dispatcher.test.ts::*::rebase-only move → ONE model turn, review posted pinned to the NEW head with the carried footer, thread told, no worktree move, src/core/dispatcher.test.ts::*::releases the executor's workspace when the run ends: if-clean for a coding run, always for a read-only agent
18: startRequestRoot — the root at receivedAt with the channel; a bound run gets the setup backfilled then live and the root its runId; log-only spans reach only the log sink; a bound card gets display labels and the clear at run.agent; spansSoFar is the streamed children, open ones open; no config → no log sink; channelOf[unit] src/core/requestTrace.test.ts::startRequestRoot::*
18: the shape line prints every non-zero bucket with the residual last and sums to the total, nothing under two informative buckets; the card's gate (a minute, or 15 s getting ready); an open span on a live window; the queued captions from a minute[unit] src/core/runShape.test.ts::shapeLine::*, ::queuedCaption::*
18: the queued numbers are on the request root AT START — originAt (the platform's stamp) becomes queuedBeforeMs, clamped at 0, and a fresh turn's wait becomes queuedBehindMs, both in the root's span_start and so in a bound run's record, which is what the page's caption reads; a root started without them carries neither[unit] src/core/requestTrace.test.ts::startRequestRoot — the queued numbers are on the root at start::*, src/core/dispatcher.test.ts::no gaps: every awaited step runs inside a span (docs/reference/specs/tracing.md)::a fresh turn for unconsumed follow-ups is a request of its own…
18: every close carries the elapsed time; the setup label rides live frames until cleared; shape and queued lines lead a close's detail[unit] src/core/statusCardFrame.test.ts::createCardShell — every paint comes from one builder::a close before the run started paints…, ::a setup label rides live frames…, ::a close's shape and queued lines lead its detail…
18: no gaps — a general run, a coding run with a tool, a refusal, a runless command, a fresh turn: every awaited fake under a span, the partition equal to the ticks per bucket with zero overhead, the root's status/runId/queuedBehindMs, the fresh turn's root after the first's end[unit] src/core/dispatcher.test.ts::no gaps: every awaited step runs inside a span (docs/reference/specs/tracing.md)::*
18: the adapters stamp receivedAt and hand their root to dispatch[unit] src/channels/http.test.ts::handleIngressRequest (transport gating + dispatch)::valid token → dispatch called with the namespaced IncomingMessage; reply returned, src/channels/mcp.test.ts::handleMcpRequest — tools/call::builds the mcp:-namespaced IncomingMessage and returns the reply as tool content
18: run_meta carries the trace id; a command run's names agent command and the trace, no model[unit] src/core/dispatcher.test.ts::live run-view wiring (Area 2)::publishes the request as a redacted \input` event before any tool event, with an attachment suffix, ::inline command runs + run receipts …::`friction report --min-runs 2` is the same run…`
5: the analyzer partitions a finished window from the same span set it times findings with, and the done card reads that shape — see run-friction.md items 3–4 rows; formatShape prints a computed partition[unit] src/core/runFriction.test.ts::analyzeRunFriction — the window, the shape and the span set (docs/reference/specs/tracing.md)::*, src/core/runShape.test.ts::shapeLine::*
19: the collector — offsets from the request start, duration, exit and timeout, the mutex wait ending where the lock was taken; names sanitized; the newest kept past the count cap and never past the byte cap; skew never negative; copies out[unit] src/execution/residentStepTrace.test.ts::createStepTrace::*
19: the bot's sanitizer rebuilds every step from the allowlist (hostile names, malformed numbers, error text, an out-of-range exit code all fall away), is bounded, tolerates a non-array; the grafter puts <prefix>.<name> children under the parent, rebased and clipped, with the attrs and the infra classification, and the clock skew on the parent[unit] src/execution/residentTrace.test.ts::sanitizeGraftedSteps::*, ::graftResidentSteps::*
19: Span.graft records a child with both stamps at once, never reads the clock, carries a classification without a message, and reads an inverted interval as zero[unit] src/core/trace/tracer.test.ts::createTracer::graft records a child with both stamps supplied…
19: the binding carries the sanitized trace and attachMs; the op result its trace and residentMs; a trace-less answer has neither[unit] src/execution/resident.test.ts::ResidentExecutor.attach over a heartbeat stream …::carries the resident's step trace on the binding…, ::ResidentOperations.run::carries the resident's step trace and total on the result, sanitized
19: the selection hands the trace and total to the dispatcher; a failed attach's steps ride the sandbox fallback's selection[unit] src/execution/factory.test.ts::makeExecutor resident selection::warm probe → ResidentExecutor, attached on open, with the resident discriminant set, ::refreshing probe then a mirror-busy attach (503) → per-thread fallback with the named attach-failed note
19: a refused attach's steps are pinned on the thrown error, sanitized, and found through a chain of causes; a trace-less refusal pins nothing[unit] src/execution/resident.test.ts::ResidentExecutor.attach over a heartbeat stream …::pins a refused attach's step trace on the thrown error…, src/execution/residentTrace.test.ts::withResidentTrace / residentTraceOf::*
19: a failed resident attach's steps graft under the failed attach span, before it ends, on the process sink[unit] src/core/dispatcher.test.ts::executor provisioning by agent resources::a failed resident attach's step trace grafts under the failed dispatch.workspace.attach span…
19: on a real dispatch the attach's steps stream as dispatch.workspace.attach.<step> under the attach span, rebased to its start, clipped to its end, before the request, with the backend and the parent's clock skew[unit] src/core/dispatcher.test.ts::executor provisioning by agent resources::a resident attach's step trace lands on the run's stream…
19 live: a resident-backed run's record carries dispatch.workspace.attach.* spans whose durations match the resident's /attach log lines; friction analyze over its stream shows them in getting ready[agent] after deploy: run agent:review on a resident-onboarded PR, GET /runs/:id/events, look for span_end records named dispatch.workspace.attach.clone/.install/.mutex_wait under the attach span; receipts on the tracker
20: a process root is named, starts at the clock or the given stamp, carries the caller's attrs and ends on the leading sinks only; withProcessRoot ends it ok on return and failed (classified, message redacted) on a throw that still propagates; no sinks and no config is silent[unit] src/core/requestTrace.test.ts::startProcessRoot::*
20: slack.catch_up, drain, deploy.step.<worker>, deploy.wait_live, resident.fleet_refresh and dashboard.residents are log-only — no class, never streamed[unit] src/core/trace/streamSpans.test.ts::streamSpans::every enumerated name and every prefix family has a class under both owners; log-only names have none
20: a fleet-facts read runs under a resident.fleet_refresh root handed to the admin client, with the listing's httpStatus and residents; a residents page request runs under a dashboard.residents root (route, httpStatus) handed to the client, ended error on a non-200 or a throw; without trace deps neither is traced[unit] src/core/residentFleet.test.ts::watchResidentFleet — the read is traced (docs/reference/specs/tracing.md item 20)::*, src/channels/residentsView.test.ts::createResidentsViewHandler — each page request is a root (docs/reference/specs/tracing.md item 20)::*
20: a deploy step is a deploy.step.<worker> root on the runner's log with its outcome, the gate a deploy.wait_live child whose waitedMs is the seconds the live line prints; a gate that never holds ends the step in error with not_live and no waitedMs[unit] src/deploy/sandboxGateRun.test.ts::deployStep (sandbox)::the step is a \deploy.step.sandbox` root…, ::a gate that never holds is the step's failure with the last reason — deployed but NOT live`
20: the clock allowlist records the shrink exactly — no listed file grew, none reads more than recorded[unit] src/core/trace/clockAllowlist.test.ts::clock ratchet::the allowlist matches the tree exactly…
8: the allowlist is empty and the tree agrees — no production file reads the clock directly[unit] src/core/trace/clockAllowlist.test.ts::clock ratchet::the allowlist matches the tree exactly…
8: the web's one clock — wallNow reads the browser clock; useWallClock starts at the seed's value or the clock, ticks every second while mounted and stops on unmount[unit] web/src/lib/wallClock.test.ts::wallClock::*
20 live: a bot connect logs one {"span":"slack.catch_up",…} line with its counts; a deploy's log prints deploy.wait_live with a waitedMs matching its live (…; Ns after the upload) line; a SIGTERM logs one drain line naming the signal, the runs held and handed[agent] after deploy: the bot's container log at the rollover (the previous generation's drain line, the new one's slack.catch_up line) and the deploy-production job log; receipts on the tracker
21: internalHostsOf collects exact hosts with ports, lowercased, deduplicated, skipping absent or unparseable URLs; never a suffix match[unit] src/core/trace/tracedFetch.test.ts::internalHostsOf::*
21: tracedFetch spans the call under the parent with host/route/method/status and never a query, a header or a body; sets traceparent for an internal host only; is a plain fetch without a parent; reads the process's set by default; ends before the body is read; classifies a transport failure (timeout on abort) with no message; tolerates an unparseable URL or an unknown method[unit] src/core/trace/tracedFetch.test.ts::tracedFetch::*
21: the tracing executor hands each exec.* span to the inner executor[unit] src/execution/tracingExecutor.test.ts::TracingExecutor::times exec / readFile / writeFile as exec.* spans under the tool's span…
21: the resident client's attach, exec and status probe are http.client children of the caller's span, the trace context set for the configured resident host and never when the set is empty or there is no span[unit] src/execution/resident.test.ts::ResidentExecutor trace context::*
21: the sandbox client's send is an http.client child with the trace context for the configured host; a call without a span is a plain fetch[unit] src/execution/cloudflareSandbox.test.ts::CloudflareSandboxExecutor trace context::*
21: no container edge reads an inbound context header[unit] src/core/trace/traceparent.test.ts::the container edges::*
21: a firing's traceId is optional and a string[unit] src/core/schedules.test.ts::isScheduleFiring::*
22: the Scheduled panel carries a firing's trace id when the recorder stored one and shows it shortened with the full id on hover[unit] src/channels/scheduledPanel.test.ts::buildScheduledRows::carries the firing's trace id…, web/src/pages/scheduled.test.ts::ScheduledPage::shows the last firing…
21 live: a resident-backed run's span log shows http.client lines under exec.exec with the resident's host and route; the resident Worker's request log shows the same trace id arriving in traceparent; a GitHub call in the same run shows no traceparent (no http.client yet — the next PR)[agent] after deploy: tracing.log: slow on the container for one run, then the resident Worker's log for the same trace id; receipts on the tracker
22: a root started with a remote parent carries its trace id and parent span id and its children follow; without one it mints its own[unit] src/core/trace/tracer.test.ts::createTracer::a root started with a remote parent…
22: adoptedParent reads a well-formed traceparent and nothing else; stripTraceContext removes the three context headers and keeps every other header, the method and the body; withTraceContext sets the span's own[unit] src/core/trace/workerTrace.test.ts::adoptedParent::*, ::stripTraceContext / withTraceContext::*
22: shimRoute maps every path to a closed-table word — never an id or a query — and gives assets, the favicon and the SSE stream no root[unit] src/core/trace/workerTrace.test.ts::shimRoute::*
22: the refusal filter drops a root that ended 401 or 403 and passes the rest; the Worker sink is slow with the filter on[unit] src/core/trace/workerTrace.test.ts::refusalFilter / workerLogSink::*
22: startAdoptedRoot joins the caller's trace when the header parses and mints its own otherwise[unit] src/core/trace/workerTrace.test.ts::startAdoptedRoot::*
22 live: a resident-backed run's bot log, the resident's log and the sandbox's log share one trace id per attach/exec; the shim's log shows bot-shim.fetch roots with table words and no ?t= token, a cron.<schedule> root per firing whose trace id the Scheduled panel's firing carries, and nothing for a 401[agent] after deploy: wrangler tail on each Worker during one review run and one cron firing; receipts on the tracker
23: a view's requests are github.rest children of the span with host/route/method/status and no path, query or token; the token resolver receives the span; the unbound client spans nothing; no traceparent leaves for GitHub[unit] src/execution/githubApi.test.ts::RestGithubApi.withSpan::*
23: the mint under a span is a github.token_mint child with scope/cached/expiresInMs and its request a github.rest child; a cache hit is a mint span with cached: true and no request[unit] src/execution/githubApp.test.ts::resolveGithubToken::under a span the mint is a github.token_mint child…
23: the runner hands a tool call the client's withSpan view for that call's span, or the client as is when it has none[unit] src/runner.test.ts::model turn and tool spans (docs/reference/specs/tracing.md)::a tool call's github capability is the client's withSpan view…
23: tracedFetch names the span as the caller asks, with the same attrs and no trace context for a foreign host[unit] src/core/trace/tracedFetch.test.ts::tracedFetch::a named client's span carries its name…
24: a view of the admin client makes http.client children of its span with the route literal (never the status query), method and status and no bearer; the trace context rides for the configured host; the unbound client is a plain fetch[unit] src/core/residentAdmin.test.ts::makeResidentAdminClient trace context::*
24: the registry hands the handler the span invoke was given and no span key otherwise, bindCommands forwards it, and invokeChatCommand forwards the dispatcher's[unit] src/core/commandRegistry.test.ts::CommandRegistry.invoke — the caller's span (docs/reference/specs/tracing.md item 24)::*, src/core/commandChat.test.ts::invokeChatCommand — the dispatcher's span (docs/reference/specs/tracing.md item 24)::*
24: adminOf binds the client to the handler's span through withSpan, never without a span, and uses a double without the view as is; a dispatched repo list binds it to run.command[unit] src/core/commands/repo.test.ts::adminOf — the command's span (docs/reference/specs/tracing.md item 24)::*, src/core/dispatcher.test.ts::*::repo list binds the admin client to the command's run.command span through withSpan (docs/reference/specs/tracing.md item 24)
24: memoryContextBlock hands its span to every scope's retrieve and nothing without one; the memory store's retrieve under a span is an http.client child with route /retrieve, the trace context for the configured host, and a plain fetch without one[unit] src/core/memory/memory.test.ts::memoryContextBlock — the caller's span (docs/reference/specs/tracing.md item 24)::*, src/core/memory/workerStore.test.ts::WorkerMemoryStore trace context::*
24: the history writer hands its span to the store's put and nothing without one; the run store's put under a span is an http.client child with route /runs/put, the trace context for the configured host, and a plain fetch without one[unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::write hands its span to the store's put…, src/core/runStoreWorker.test.ts::WorkerRunStore trace context::*
24 live: a repo rebuild in Slack logs http.client lines with route: "/rebuild" under its run.command; a run's dispatch.memory_read has /retrieve children; its record's /runs/put line carries the request's trace id[agent] after deploy: the bot's log at tracing.log: slow (or a slow Worker) during one command and one review run; receipts on the tracker
25: residents and swept are numeric attrs in the closed table[unit] src/core/trace/streamSpans.test.ts::attrs::*
25 live: a resident watchdog firing logs one resident.watchdog root with residents and its resident.check children, the recorded firing carries that trace id, a refresh cycle logs resident.refresh with its resident.<step> children, and the state Worker's sweep logs state.alarm with swept[agent] after deploy: wrangler tail switchboard-resident across one cron minute and wrangler tail switchboard-memory across one sweep interval; receipts on the tracker
26: the span log keeps every span end as the log line plus endedAt, oldest first, at any level; filters by trace id, span name or family, and end time; the newest limit of what matched, capped; bounded by lines and bytes with the drop count[unit] src/core/trace/spanLog.test.ts::createSpanLog::*
26: GET /admin/trace/log — a trace:read bearer gets the lines, counts and oldest stamp with the filters applied; no bearer 401, the wrong grant 403, no token map 503, a non-GET 405, a malformed filter 400, none of them reading anything[unit] src/channels/adminTraceLog.test.ts::GET /admin/trace/log::*
26: the span log's sink joins the leading sinks beside the log sink whatever the level, and injected sinks win outright[unit] src/core/requestTrace.test.ts::startRequestRoot::the process's span log joins the leading sinks…
26 live: curl -H "authorization: Bearer <ingress token with trace:read>" "https://<bot host>/admin/trace/log?span=github&limit=50" during a run that used a github_* tool returns github.rest lines with route words and a github.token_mint line; the same request without a bearer is 401[agent] after a deploy, against this installation's bot host.