Engineering reference / desktop 1.12.2 baseline + local telemetry changes
Implementation update · 24 September 2026. Section 10 documents the attributes now added in this workspace. Sections 0–8 retain the original 1.12.2 audit, mock trace, measurements and candidate list for comparison. In sections 0–8, “current” coverage means the baseline before these changes. Sections 9–10 reflect the simplified implementation. This is not a deployed release.

From shortcut
to delivered text.

A complete map of the normal desktop dictation path: what runs, what overlaps, what blocks the first chunk, and what survives into PostHog.

One event: transcription_completed_v2D · elapsed durationO · offset from session openΣ · per-session aggregate

0Read the trace

The time origin is session open

T0 = openSessionTrace() during the IDLE → STARTING snapshot. Input handling, start admission, and the synchronous prefix of transcription.open() happen before this anchor.

offset = timestamp − T0

T0 is not physical key-down, mic acquisition start, or recording-live. UI buttons and hands-free toggles can also start a session.

PostHog receives a summary

The client collects named spans, synthetic durations, points, and chunk statistics. At flush it builds one flattened event. Raw startedAt, endedAt, span IDs, and the span tree are not included in that event.

duration = span end − span start

A 261 ms mute_duration_ms says how long the phase took. It does not locate its start or end relative to the shortcut.

Illustration, not a sampled production trace. The waterfall and example payload below use deliberately constructed times to show ordering and overlap. They are not cohort percentiles. Section 4 contains the separate local measurements.

Scope: a successful live dictation with sounds enabled and automatic paste. The native audio detail is the current macOS AVAudioPlayer implementation. The AVAudioEngine experiment is not part of this flow. Other outcomes and draft mode are covered in section 6.

1Inspect the trace waterfall

Select a bar or diamond to see its exact boundaries, PostHog mapping, and limitations. Gray and dashed bars show supporting work; they are not all real named spans.

Before T0 · duration unknown in PostHog
Physical shortcut → native key event → Electron shortcut manager → input grammar / verb queue → admission checks (including selected model) → STARTING snapshot → stream open / warmup launch → trace opens. The admission lookup has a 3 s bound; it is not a fixed startup delay.
ALL WATERFALL TIMES ARE ILLUSTRATIVE
Duration exportOffset exportAggregate exportLocal / internal onlyNo separate timing export
Three different readiness points: capture confirmation produces recording_live_offset_ms; the first PCM arriving in main ends recorder_spinup_duration_ms; the first frame admitted past readiness and the startup audio gate produces first_chunk_offset_ms. That last timestamp is set before VAD, provider work, and transcription-lock processing.

2The actual desktop sequence

Startup branches overlap. On a normal stop, recorder closure permits transcript resolution while WAV closure and audio restoration can continue. Storage finishes before automatic delivery is staged.

Input → session
Shortcut / UI → admission → STARTING → trace T0
Parallel startup
Mic + worklet ∥ start sound + mute ∥ provider warmup
Recording
PCM → readiness + startup audio gate → VAD → provider, with WAV writes alongside
Stop → resolve
Final frame → recorder closed → drain → flush → format; close WAV and restore audio alongside
Settle → deliver
Commit → dispatch paste → IDLE → native reply → telemetry flush

Loading the detailed sequence diagram. The flow summary above remains available if the diagram CDN cannot load.

2.1 · What the startup audio gate actually waits for

Recorder.start() immediately sets beepPending = true. Main reads preferences and sends startRecording. On macOS the helper serializes ambiance operations, waits for rec-start playback completion, calls the system-mute setter if enabled, and replies. The promise then releases the gate.

Non-final frames are dropped while the recorder is still starting or the startup audio gate is closed. First-PCM liveness is measured before these checks. A final frame bypasses the startup audio gate, but not the recorder-starting check. If dictation sounds are muted, the gate releases after reading preferences without waiting for the native RPC.

The mute setter returning successfully does not prove physical silence. The implementation does not wait for a CoreAudio property-change notification.

2.2 · Resolution and backend time

resolve.drain waits for already admitted chunk work. resolve.flush then obtains the transcription lock and flushes the provider. For cloud dictation, this can include waiting for a backend final result and cloud formatting. resolve.format prepares the returned text and may perform additional configured formatting.

resolve_duration_ms covers this whole chain plus other work in the resolve effect. It is not a pure server-latency metric. Provider warmup and server-internal spans are not separately exported by this desktop event.

3What reaches PostHog

Event: transcription_completed_v2. Ten named phase durations, a separate session duration, and four offset fields can be emitted. Fields are conditional on which work ran and finished in time; a missing field is not zero.

3.1 · Durations and offsets

PostHog fieldSource span / pointWhat is actually measured
Durations are not positions on a shared timeline. Recorder spinup begins at recorder.start(). Native-start timing begins inside ambiance.begin(). Only exported offsets use the session-open anchor. Do not add parallel phases, or subtract population percentiles to estimate a per-session gap.

3.2 · Chunk aggregates and descriptive fields

FieldsMeaning / caveat
chunk_countCount of calls admitted into streaming-chunk processing. Frames dropped by readiness or beep gating are not included. This is not a count of server requests.
vad_duration_sum_ms
vad_duration_max_ms
Sum and maximum of per-chunk VAD timings. The timer starts before the VAD lock, so waiting for that lock is included. These are aggregates, not a single contiguous span.
transcribe_duration_sum_ms
transcribe_duration_max_ms
Sum and maximum of per-chunk provider transcribe() calls. Starts inside the transcription lock after materialization; excludes the prior lock wait. Meaning depends on the provider, and may include local buffering or enqueueing rather than a complete remote transcription.
materialize_duration_msOne-time setup on the first processed chunk: load dictation context and configuration, select model, open and attach the provider session. Timing starts inside the transcription lock. Zero can mean no materialization was recorded.
model_id, providerRecorded model/provider when the session materializes. No warmup duration or connection-ready offset accompanies these fields.
audio_input_channel_count
audio_track_channel_count
audio_stereo_downmix_enabled
Copied from the lifecycle.audio-capture point. Format metadata only; that point's timestamp is not exported.
session_id, disposition, flush_reason
failed_stage, error_code, error_tag, defect
Identity, outcome, and failure attribution. The terminal-latch point can supply failure details, but no latch offset is exported. App/version/machine/system metadata is added through the telemetry envelope.

3.3 · The flattened event for this mock session

All numbers below are illustrative and match the waterfall. Routine identity/system properties are omitted. Notice that pasted_offset_ms can exceed session_duration_ms because native confirmation may arrive after the lifecycle reaches idle.

Expand example PostHog payload
Derived values that are safe to interpret

first_chunk_offset_ms − recording_live_offset_ms measures the elapsed time from recording-live to first chunk admission within the same session. It can include frame buffering, IPC/scheduling, and beep gating; it does not isolate any one of them.

pasted_offset_ms − last_chunk_offset_ms measures last admitted chunk → native paste confirmation. It is not exact shortcut-release → paste latency: the stop input can precede the final admitted frame.

A native paste acknowledgement means the helper reported success posting the paste command. It does not verify insertion or timestamp when another app renders the text.

4The local delay we measured

This is separate evidence from the illustrative waterfall: a recording in the installed desktop app's local log on this M4 Max, 23 September 2026 at 13:21:41 IST. Its time origin here is the native RPC dispatch, not T0 or shortcut press. The installed app was reporting 1.12.0.

248 ms
Native start request → helper response
243 ms
Sound request → subsequent mute request; includes setup
156 ms
Native request → first PCM observed in renderer
Position from native RPC dispatchObserved event
0 msMain sends startRecording.
3 msRenderer begins capture startup.
5 msNative sound request begins.
45 msplay() has returned successfully; the code logs “Playing sound”. This is not a measured acoustic onset.
128 msRenderer graph setup has completed. Its local total-startup timer reports 125.8 ms.
156 msRenderer sees its first PCM frame; the native playback wait is still in progress.
248 msPlayback completion has released the native wait; mute setter succeeds and helper replies. The mute call is below the log's 1 ms resolution, not a measurement of physical silence.

In that capture startup: getUserMedia 83.9 ms, AudioContext resume 36.2 ms, first-frame wait 28.7 ms. Those renderer operations overlap the native sound path.

4.1 · Standalone playback experiments

The sound contains 99.68 ms of audio. A standalone AVAudioPlayer probe reached the end of its playback timeline about 121–123 ms after play() returned, but the completion callback arrived at about 201–214 ms. A WAV version and prepareToPlay() did not remove the callback wait. play() itself took roughly 16–19 ms warm and 92 ms in one cold run.

A separate AVAudioEngine prototype using the same sound and .dataPlayedBack completed in 142–150 ms while warm and 157–168 ms after pausing between plays. These are isolated local probes, not an integrated desktop change or a population result. The current source still uses AVAudioPlayer.

5What the current event cannot tell us

Before and during capture

  • Physical key-down → accepted start / trace open.
  • Renderer scheduling, capture-mutex wait, and config IPC individually.
  • Device enumeration, getUserMedia, AudioContext resume, worklet load, and graph setup as separate PostHog timings. Several are already measured in local console logs.
  • First PCM arrival as an offset from T0; only recorder-spinup duration is exported.

Sound, mute, and ending

  • Native request start/end offsets or sound-enabled setting per trace.
  • Sound setup / audible start / playback completion / mute setter as separate timings.
  • Beep-gate release, frames dropped by the gate, and confirmed device mute.
  • Stop input / recording-stop offset and exact backend-internal stages.
5.1 · Proposed additions — not currently implemented

The detailed, source-audited proposal is now in section 8: each interval has its boundary, a concrete reason it could exceed 20 ms, a local measurement or explicit gap, and its current v2 coverage.

Section 9 defines the shared names and clock contract. It preserves session-open T0 and uses shared epoch timestamps for offsets with local duration measurements. Native timing is limited to caller-observed RPCs. The event names and proposed field pairs there supersede the earlier sketch.

6Branches and trace-flush rules

The lifecycle ends before every tail must end

The root duration stops when the public state returns to idle. Telemetry waits for registered obligations, including recorder closure, storage, native audio restoration, and a dispatched paste response.

Flush occurs once: when the root is closed and all expected obligations settle, or after a 15 s grace period from root close. Late records after flush are dropped. Shutdown force-flushes remaining traces.

Presence does not always prove success

An Effect span may end with failure or interruption and still supply a duration. Some best-effort spans catch their own errors. By contrast, delivery.pasted is recorded only for a confirmed successful native paste.

The flat payload takes the first mapped duration/offset for each key. Individual span status and timestamps remain in internal/debug records; failure attribution is summarized separately.

6.1 · Quick release, cancel, empty, and failure

A quick release or interrupted start can seal a discard before usable audio exists. Empty, dismissed, failed, and discarded sessions still go through settlement and emit the session summary if the app gets to flush it. An admission refusal before session creation does not create this trace.

Capture failures and provider terminal failures can seal the result during STARTING or RECORDING. The happy-path finalization and paste spans may therefore be absent. Cancellation can retire chunk work before late timings finish.

6.2 · Draft mode and post-session actions

On stop, draft mode may wait for a bounded clipboard-copy capture barrier of up to 2.5 s before entering the measured transcription.resolve effect. That wait is not a dedicated exported timing.

Successful draft sessions stage text for review instead of immediately pasting. The later “confirm draft” paste and “paste latest transcription” action are outside this live session's delivery.pasted timing.

6.3 · Two different drains

The renderer/recorder drain flushes the worklet, accepts final in-flight PCM, then emits recorderClosed. There is no dedicated PostHog duration for that stop-to-close-decision interval.

resolve.drain begins after that decision and waits for chunk-processing tasks already admitted to transcription. recorder_close_duration_ms is another distinct measurement: the asynchronous WAV write/finalize/enrichment tail, which may overlap resolve.

7Source map

Paths are relative to /Users/z/exa9/@amical/amical. This document describes revision 097ffe4; local experimental results were collected on 23 September 2026.

Trace fields, anchors, and flushapps/desktop/src/main/telemetry/dictation-trace.ts:58apps/desktop/src/main/telemetry/dictation-trace.ts:129apps/desktop/src/main/telemetry/dictation-trace.ts:407
Start admission, T0, recording-live, idle closeapps/desktop/src/main/lifecycle/runtime.ts:354apps/desktop/src/main/lifecycle/runtime.ts:445apps/desktop/src/main/lifecycle/runtime.ts:481apps/desktop/src/main/lifecycle/runtime.ts:524
Native startup/restoration and draft barrierapps/desktop/src/main/lifecycle/live.ts:99apps/desktop/src/main/lifecycle/live.ts:158apps/desktop/src/main/lifecycle/live.ts:205
Capture, readiness, and 32 ms PCM framesapps/desktop/src/hooks/useAudioCapture.ts:212apps/desktop/src/hooks/audioCaptureDevice.ts:24apps/desktop/src/hooks/audioCaptureContext.ts:6apps/desktop/src/hooks/audioCaptureWorklet.ts:17apps/desktop/src/assets/audio-recorder-processor.js:4
Gate, spinup, WAV custodyapps/desktop/src/main/lifecycle/adapters/recorder.ts:182apps/desktop/src/main/lifecycle/adapters/recorder.ts:253apps/desktop/src/main/lifecycle/adapters/recorder.ts:336
Streaming and resolutionapps/desktop/src/main/lifecycle/adapters/transcription.ts:119apps/desktop/src/services/transcription-service.ts:567apps/desktop/src/services/transcription-service.ts:941
Storage and paste confirmationapps/desktop/src/main/lifecycle/adapters/storage.ts:172apps/desktop/src/main/lifecycle/adapters/host.ts:110apps/desktop/src/main/lifecycle/adapters/host.ts:157
macOS sound and mutepackages/native-helpers/swift-helper/Sources/SwiftHelper/RecordingAmbianceController.swift:22packages/native-helpers/swift-helper/Sources/SwiftHelper/AudioService.swift:142packages/native-helpers/swift-helper/Sources/SwiftHelper/AccessibilityService.swift:146Apple: asynchronous property-set semantics
Local measured evidence/Users/z/Library/Logs/Amical/amical.logRecording beginning 2026-09-23 13:21:41 IST. Native-helper and standalone Swift playback probes were separate diagnostic runs.

8Proposed spans: evidence before implementation

PROPOSAL ONLY · NO TELEMETRY ADDED

The earlier list was too broad to treat every item as an established >20 ms bottleneck. Below, measured slow work, concrete waits, and lower-priority diagnostic candidates are distinguished explicitly. Existing span names stay unchanged; new names follow the same dotted namespace and hyphenated operation convention.

Local evidence: 31 captures in this machine's installed-app log, 23 September 2026, 04:38–14:09 IST. The latest explicit preceding version is 1.12.0. These are local observations, not 1.12.2 population statistics. Values are median and observed min–max, in milliseconds. Cold context operations have n=12; resumed contexts have n=19.

“Not measured separately” means unknown, not zero. The log contains no “Dictation trace” debug records, so missing v2 phase values cannot be recovered from it. No number from the illustrative waterfall is used in these tables. A faster CPU does not shorten the audio file, remove the 32 ms frame size, or determine backend latency.

8.1 · Input and renderer capture
Span and exact boundaryWhy it could exceed 20 msThis machineCurrent v2 coverage
lifecycle.recording-start-queue-wait

Start request enqueued → admission begins.

Can wait behind earlier asynchronous lifecycle verbs. Contention candidate; not inherently slow.Not measured separately.Not exported
lifecycle.recording-start-checks

Admission begins → start accepted/refused.

Selected-model settings read and possible migration/write; 3 s guard. Ordinary local reads are expected to be short. Lower priority.Not measured separately.Not exported
capture.dispatch

Main publishes starting → renderer enters startCapture().

Process scheduling and React work can delay handling. This is a cross-process interval, not pure wire/IPC latency.Not measured separately.Not exported
capture.lock-wait

startCapture() requests mutex → its body enters.

An earlier stop, context close, or cleanup can still hold the capture mutex.Not measured separately.Not exported
capture.config-read.rpc

Renderer starts cached-config query → query settles.

Cached memory read itself is small; only queueing, IPC scheduling, or an event-loop stall justifies >20 ms. Lower priority.Not measured separately.Not exported
capture.enumerate-devices

enumerateDevices() call → result.

Actual slow samples exist on this machine: 5/31 exceeded 20 ms. Device discovery crosses browser/OS boundaries.2.1 ms median; 0.3–63.9 ms; n=31.Not exported
capture.get-user-media

getUserMedia() call → stream returned or error.

Microphone permission/device opening and browser/OS audio setup. 31/31 exceeded 20 ms.83.9 ms median; 68.9–137.7 ms; n=31.Not exported
capture.audio-context-create

AudioContext constructor entry → return.

Creating the audio engine opens/initializes browser and OS audio resources. 12/12 exceeded 20 ms.35.0 ms median; 32.8–37.2 ms; n=12 cold contexts. Excludes worklet loading.Not exported
capture.audio-context-resume

AudioContext.resume() call → promise settles.

Audio engine/device restart and scheduling. 19/19 exceeded 20 ms.36.2 ms median; 34.6–40.9 ms; n=19 warm contexts.Not exported
capture.worklet-load

audioWorklet.addModule() call → promise settles.

Cold module loading, evaluation, and audio-thread scheduling can be slower on other machines. No >20 ms sample here. Conditional candidate.2.45 ms median; 1.8–16.7 ms; n=12; 0/12 >20 ms.Not exported
capture.graph-setup

Source/worklet construction → graph and analyser connected.

No evidence of intrinsic >20 ms cost. Keep inside capture-startup coverage unless an actual slow sample appears; not a priority standalone span.Full interval not measured. Node construction alone: 0.1 ms median; 0.0–0.7 ms; n=31.Not exported
capture.first-frame-wait

Frame handler attached → first nonempty renderer PCM frame observed.

Regular frames contain 512 samples at 16 kHz: 32 ms of audio. Includes audio scheduling and the final graph-connection work after handler attachment.34.0 ms median; 27.3–46.7 ms; n=31; all >20 ms.Not exported
capture.first-frame-ipc

Renderer sends first PCM → main receives it.

Ordinary local IPC should be short. >20 ms would mainly indicate process scheduling/backlog. Cross-process interval; lower priority.Not measured separately. First renderer PCM was 156 ms after native-start RPC dispatch in the 13:21 sample; that is not IPC duration.Not exported
lifecycle.recorder-spinup

recorder.start() → first nonempty PCM observed in main.

Existing parent includes renderer scheduling, acquisition, context work, buffering, and IPC.Not measured separately. Renderer startup diagnostic is a different interval: 126.4 ms median, 110.2–221.2 ms, n=31.Duration: recorder_spinup_duration_ms
8.2 · Native sound, mute, and gate — internal candidates deferred

Current scope: main measures RPC duration and start offset. Internal native spans below are candidates only; they are not implemented.

Span and exact boundaryWhy it could exceed 20 msThis machineCurrent v2 coverage
lifecycle.ambiance-preferences

Preference read begins → sound/mute settings available.

Local settings read; no evidence of normal >20 ms work. Lower priority, useful only to explain a parent-span gap.Not measured separately.Not exported
native.start-recording.rpc

Main calls startRecording → helper response/error.

Includes dispatch, helper scheduling, start sound, and mute. Every measured round trip exceeded 20 ms.254 ms median; 218–285 ms; n=31.Contained in mute_duration_ms; exact RPC not separately exported
native.start-recording.queue-wait

Helper start operation requests ambiance mutex → lock acquired.

Can wait behind a prior stop sound/restoration operation. Actual queue wait is not separately logged.Not measured separately.Not exported
native.start-recording.sound-setup

Native sound work starts → play() returns.

Per-play AVAudioPlayer creation and playback setup; 29/31 observed setup intervals exceeded 20 ms.≈47 ms median; 9–64 ms; n=31. Wall-log proxy, includes work before player creation.Not exported
native.start-recording.sound-wait

play() returns → playback completion is processed.

The sound itself contains ≈99.68 ms of audio; completion also includes output/callback scheduling. Faster CPU cannot remove the sound duration.≈203 ms median; 199–203 ms; n=31. Proxy endpoint is the subsequent mute request, not a direct callback timestamp.Not exported
native.start-recording.system-mute

Mute setter call → returned result.

No >20 ms evidence here. Device/driver stalls are possible; retain as a small diagnostic child to distinguish actual mute cost from playback.≈1 ms median; 0–2 ms; n=31; 0/31 >20 ms. Physical silence is not measured.Not exported
lifecycle.recorder-start-gate-wait

Recorder start sets beepPending → gate clears.

Normally waits for the start operation when sounds are enabled. Regular frames are dropped while closed; sounds-disabled path opens earlier.Not measured separately. Do not substitute native RPC duration.Not exported
lifecycle.mute-ambiance

Existing ambiance.begin timer → start operation resolves.

Parent includes remaining preference read, native request, sound completion, and optional mute.Not measured separately. Sound-request → mute-request proxy alone: ≈250 ms median, 211–264 ms, n=31.Duration: mute_duration_ms
lifecycle.unmute-ambiance

End ambiance effect begins → native stop settles/error handled.

Can first join a pending start, then restore audio and await the stop sound.Not measured separately. The narrower native stop RPC is measured below.Duration: unmute_duration_ms
native.stop-recording.rpc

Main calls stopRecording → helper response/error.

Restoration plus stop sound and helper scheduling. 31/31 exceeded 20 ms. Excludes any main-side wait for pending startup.261 ms median; 218–270 ms; n=31.Contained in unmute_duration_ms; exact RPC not separately exported
native.stop-recording.queue-wait

Helper stop operation requests ambiance mutex → lock acquired.

May wait for an earlier start/stop operation that is still playing a sound.Not measured separately.Not exported
native.stop-recording.system-restore

Restore original output properties → API result.

No measured >20 ms case isolated here. OS/device calls can stall; diagnostic child, not a known bottleneck.Not measured separately.Not exported
native.stop-recording.sound-setup

Stop-sound work starts → play() returns.

Same player setup as start; 26/31 observed setup intervals exceeded 20 ms.≈56 ms median; 10–65 ms; n=31.Not exported
native.stop-recording.sound-wait

play() returns → stop-sound completion processed.

Waits for audio playback and the callback. The >200 ms stop RPC establishes a slow parent, but does not isolate this child.Not measured separately. Do not subtract medians to estimate it.Not exported
8.3 · Context and streaming transcription
Span and exact boundaryWhy it could exceed 20 msThis machineCurrent v2 coverage
context.accessibility-refresh

Refresh requested → accessibility snapshot propagated into session memory.

Helper queue and accessibility calls into another app, tree walks and selection fallbacks. Foreground-app responsiveness matters.Whole interval not measured. Native extraction only: 16.80 ms median; 8.03–72.01 ms; n=31; 10/31 >20 ms.Not exported
transcription.provider-warmup

Provider warmup requested → settles.

Cloud can refresh an expired token over the network; local Whisper can load a model/worker. Warm calls may be short.Not measured separately.Not exported
transcription.vad-reset

Reset requested → VAD lock acquired and state reset.

Reset itself is small synchronous work. >20 ms would usually be waiting behind earlier inference/retry work.Not measured separately.Not exported
transcription.session-setup

Inside transcription lock: context/config loading begins → provider session attached.

SQLite context, vocabulary, snippet and settings reads plus provider-session setup. Useful setup parent; model loading is not necessarily inside this interval.Not measured separately.Duration: materialize_duration_ms, in chunk aggregate; no offset
transcription.vad-lock-wait

Chunk requests VAD lock → lock acquired.

Prior inference, reset, or history retry can hold the lock and create a backlog.Not measured separately.Included in VAD sum/max only
transcription.vad-process

VAD frame-processing call → result.

ONNX inference and its scheduling can cross 20 ms under CPU load or on slower devices. No isolated local timing in this log.Not measured separately.Included in VAD sum/max only
transcription.chunk-lock-wait

Chunk requests transcription lock → lock acquired.

Earlier materialization, inference, provider work or flush can hold the shared lock.Not measured separately.Not exported separately
transcription.provider-transcribe

Provider transcribe() called → result/error.

Local model loading/inference, or cloud auth/transport/fallback work. Some provider calls only buffer data and return quickly.Not measured separately.Transcribe sum/max only; these exclude lock wait and materialization
8.4 · Stop, resolution, storage, and paste
Span and exact boundaryWhy it could exceed 20 msThis machineCurrent v2 coverage
lifecycle.recorder-final-frame-wait

Recorder.stop() → recorder-close decision.

Waits for renderer drain/final-frame IPC. A missing final frame can reach the 3 s bound.Not measured separately.Not exported; not recorder_close_duration_ms
capture.worklet-flush

Flush request posted → final frame handed off or timeout.

Worklet/audio-thread scheduling; timeout is 1 s. Main processing of the final frame is outside this renderer wait.Not measured separately.Not exported
capture.teardown

Post-flush teardown begins → context suspended and mic/nodes released.

AudioContext suspension and audio-device/resource cleanup can wait on browser/OS work.Not measured separately. Stopping → stopped log gap was ≈1 ms in the 13:21 sample; final-frame handoff was logged 1 ms later. Neither isolates this teardown span.Not exported
lifecycle.recorder-close

Close-tail execution → queued writes, WAV finalization and duration enrichment finish.

File write queue, stream completion, header rewrite/file close, then metadata update can wait for I/O.Not measured separately.Duration: recorder_close_duration_ms
context.draft-selection-copy

Draft fallback begins → copied selection merged into session.

Native Cmd+C and clipboard polling every 25 ms, up to 300 ms; RPC is bounded at 2 s.Not measured separately.Not exported
resolve.draft-selection-wait

Resolve adapter reaches copy barrier → barrier releases/times out.

Waits for the preceding copy fallback, bounded at 2.5 s. May overlap most of the copy operation.Not measured separately.Not exported; outside transcription.resolve
transcription.resolve

Resolve effect entry → resolved result assembled.

Parent covers admitted-work drain, provider flush, formatting and remaining resolve work.Not measured separately. Stop RPC dispatch → resolved log was 1,224 ms in one sample; this broader gap is not resolve duration.Duration: resolve_duration_ms
resolve.drain

Begin waiting for admitted chunk work → that work settles.

Outstanding inference, network/provider work and lock backlogs can hold finalization.Not measured separately.Duration: resolve_drain_duration_ms
resolve.flush

Attempt transcription lock → provider flush and final-result merge finish.

Includes lock wait, final local inference or cloud result/formatting. Network/backend cost is not determined by the CPU in this machine.Not measured separately.Duration: resolve_flush_duration_ms
resolve.format

prepareTranscriptText() → completion.

Can call a separate remote formatter or Ollama when configured; otherwise often only settings/text processing. Cloud formatting may already be in resolve.flush.Not measured separately.Duration: resolve_format_duration_ms
storage.transcription-metadata-write

Post-resolve enrichment requested → metadata update finishes.

Local SQLite read/update; only I/O contention/stalls make >20 ms compelling. Lower priority.Not measured separately.Not exported
storage.commit

First disposition-commit attempt begins → result/error.

Can await WAV custody (configured 5 s bound), then perform transaction/file cleanup. Later repair retries are outside this first attempt.Not measured separately.Duration: storage_duration_ms
delivery.paste

Delivery effect begins → native paste dispatched.

Preserve-clipboard preference read and dispatch. Expected short unless local DB/event-loop work stalls. Lower priority.Not measured separately.Internal span only
delivery.pasted

Delivery effect begins → successful native paste acknowledgement.

Helper queue, clipboard format snapshot, clipboard writes and key-event dispatch can take time, especially with large clipboard data.Not measured separately.Duration: paste_duration_ms; end offset: pasted_offset_ms
Paste acknowledgement is not visible insertion. On macOS the helper returns success after posting Cmd+V; it does not verify the target application's text. Clipboard restoration scheduled 700 ms later is outside this acknowledgement. Keep the existing delivery.pasted name and pasted_offset_ms key for compatibility, but label their meaning correctly.

The existing session root is retained as the lifecycle envelope. It starts at T0 and ends at idle; outstanding native responses and other obligations can end after it. Parent and child intervals overlap, so their durations must not be added.

9Names and clocks — simplified contract

Measure existing operations first. Native RPCs are measured entirely in main. Native code, response contracts, and generators are unchanged. Internal playback/mute subphases are deferred until later analysis justifies them.

9.1 · Interval attributes

name              dotted namespace + hyphenated operation
span_id           unique within the event
parent_span_id    logical parent
process           main | renderer
start_offset_ms   epoch start timestamp − session-open epoch timestamp
duration_ms       elapsed milliseconds
status            ok | failed | interrupted
chunk_index       present for repeated chunk work

9.2 · Clock sources

Cross-process starts use Date.now() Unix-epoch milliseconds. Main and renderer share the machine's wall clock, so their offsets need no clock mapping. Local performance.now() and existing Effect timers supply elapsed durations where already used. Existing synthetic main spans can use Date differences.

start_offset_ms = startedAtMs − sessionStartMs

Renderer timing batches contain only phase name, epoch start, and elapsed duration for completed operations. The original cached-config query is retained. There is no clock-sync endpoint, uncertainty metadata, clock-change correction, or native timing payload.

9.3 · Measurement scope

10Implemented v2 attributes

The event remains transcription_completed_v2. It contains the selected main/renderer span durations and start offsets, with existing field meanings preserved. For native operations, it contains caller-observed RPC timing only.

10.1 · Payload contract

trace_spans holds the interval attributes in section 9. trace_points holds name, offset_ms, and process. The implicit root, identified by trace_root_span_id, starts at offset 0 and ends at session_duration_ms. Logical children can finish after the lifecycle returns to idle.

Example added properties — illustrative values
{
  "trace_schema_version": 1,
  "trace_offset_anchor": "session_open",
  "trace_root_span_id": "root-example",
  "trace_spans": [
    {
      "span_id": "phase-example",
      "parent_span_id": "root-example",
      "name": "native.start-recording.rpc",
      "process": "main",
      "start_offset_ms": 5,
      "duration_ms": 248.1,
      "status": "ok"
    }
  ],
  "trace_points": [
    {
      "name": "lifecycle.recording-stopping",
      "offset_ms": 3000,
      "process": "main"
    }
  ],
  "native_start_recording_rpc_start_offset_ms": 5,
  "native_start_recording_rpc_duration_ms": 248
}

10.2 · Flat duration / start-offset pairs

24 September trim: retained 12 additional spans for microphone/device startup, audio readiness, native start/stop RPCs, the recorder start gate, provider warmup, and context capture. Removed admission checks/queue, capture lock/config read, worklet load/flush, teardown/context close, VAD reset, recorder final-frame wait, WAV write/finalize, and metadata-write timers. Existing aggregate timings remain. The candidate audit in section 8 is historical; this table is the current field list.

These selected spans also have top-level properties for percentile queries. The first recorded instance supplies the flat pair; individual instances remain in trace_spans.

Expand implemented field pairs (22 spans)
Span nameEvent attributes
lifecycle.mute-ambiancemute_duration_ms
mute_start_offset_ms
lifecycle.recorder-spinuprecorder_spinup_duration_ms
recorder_spinup_start_offset_ms
lifecycle.recorder-closerecorder_close_duration_ms
recorder_close_start_offset_ms
delivery.pastedpaste_duration_ms
paste_start_offset_ms
storage.commitstorage_duration_ms
storage_start_offset_ms
lifecycle.unmute-ambianceunmute_duration_ms
unmute_start_offset_ms
transcription.resolveresolve_duration_ms
resolve_start_offset_ms
resolve.drainresolve_drain_duration_ms
resolve_drain_start_offset_ms
resolve.flushresolve_flush_duration_ms
resolve_flush_start_offset_ms
resolve.formatresolve_format_duration_ms
resolve_format_start_offset_ms
lifecycle.recorder-start-gate-waitrecorder_start_gate_wait_duration_ms
recorder_start_gate_wait_start_offset_ms
capture.enumerate-devicescapture_enumerate_devices_duration_ms
capture_enumerate_devices_start_offset_ms
capture.get-user-mediacapture_get_user_media_duration_ms
capture_get_user_media_start_offset_ms
capture.audio-context-createcapture_audio_context_create_duration_ms
capture_audio_context_create_start_offset_ms
capture.audio-context-resumecapture_audio_context_resume_duration_ms
capture_audio_context_resume_start_offset_ms
capture.first-frame-waitcapture_first_frame_wait_duration_ms
capture_first_frame_wait_start_offset_ms
native.start-recording.rpcnative_start_recording_rpc_duration_ms
native_start_recording_rpc_start_offset_ms
native.stop-recording.rpcnative_stop_recording_rpc_duration_ms
native_stop_recording_rpc_start_offset_ms
context.accessibility-refreshaccessibility_refresh_duration_ms
accessibility_refresh_start_offset_ms
context.draft-selection-copydraft_selection_copy_duration_ms
draft_selection_copy_start_offset_ms
resolve.draft-selection-waitdraft_selection_wait_duration_ms
draft_selection_wait_start_offset_ms
transcription.provider-warmupprovider_warmup_duration_ms
provider_warmup_start_offset_ms

Additional intervals in trace_spans include transcription.session-setup, transcription.provider-transcribe, and completed VAD work and lock waits. Existing VAD/provider aggregates and materialize_duration_ms retain their previous names and scope.

New point properties: recording_stopping_offset_ms (lifecycle entering stopping) and first_accepted_frame_offset_ms (main admits first PCM). Settings: dictation_sounds_enabled and system_audio_mute_enabled.

10.3 · Coverage and limits