--- title: 'The Silence Was Load-Bearing' excerpt: 'The last two releases of GeekBye v1 are about the same uncomfortable truth: realtime transcription over a real network is not lossless, and the honest move is to stop pretending it is. v1.8.20 kept a copy of every audio chunk on disk before dropping it during a reconnect, and started marking the gaps in the transcript out loud. v1.9.0 stopped sending silence to save bandwidth — and discovered the silence was the exact signal the transcriber used to know a sentence had ended. Two releases about the cost of throwing things away.' date: '2026-07-25' author: 'Steven' authorAvatar: '/images/blog/authors/steven.jpg' coverImage: '/images/blog/covers/the-silence-was-load-bearing.svg' tags: ['Engineering', 'Audio', 'Reliability', 'GeekBye Releases'] keywords: - 'preserve audio during websocket reconnect transcription' - 'tee pcm audio to disk before dropping realtime' - 'dropping silence breaks vad utterance finalization' - 'client driven commit on sustained silence elevenlabs scribe' - 'mark transcript gap reconnecting audio may be missing' - 'why partial transcript never finalizes trailing dots' lastModified: '2026-07-25' tldr: "The final two v1 GeekBye releases stopped pretending realtime transcription is lossless. v1.8.20: during a WebSocket reconnect or a provider swap (Deepgram ↔ ElevenLabs) the client drops audio chunks so the freshly-connected provider doesn't choke on stale audio — but that dropped audio used to be gone forever and silently missing from the transcript. The fix does two honest things: it tees every captured chunk to a `/sessions/session-.wav` file on disk (via `teeChunkToDisk`, a 44-byte WAV header patched on stop) BEFORE the drop gates run, so the raw audio is preserved for a future reconciliation pass; and on the first reconnect it inserts a visible transcript marker, `[reconnecting — audio may be missing]`, so the gap is named instead of hidden. Because the tee costs disk (the team estimated ~86 MB per 30-minute recording), a `StartupCleanupService` prunes those files after 7 days. v1.9.0: the client started dropping silence chunks before sending them (an efficiency + backend-contract win) — and that broke utterance finalization, because ElevenLabs Scribe's VAD needs about a second of silence in the stream to decide a sentence has ended. With the silence filtered out, partials accumulated forever and the UI showed a permanent trailing '...'. The fix re-adds the MEANING of silence as an explicit out-of-band signal: the client tracks sustained local silence and fires a `commit` after `SILENCE_COMMIT_THRESHOLD_MS = 1000`. Both releases are the same lesson from two angles — the thing you drop to be efficient (the audio, the silence) turns out to have been load-bearing." keyTakeaways: - 'During a reconnect or provider swap the client deliberately drops audio chunks so the new provider does not choke on stale audio — v1.8.20 makes that non-destructive by teeing every chunk to a `session-.wav` on disk (via `teeChunkToDisk`) BEFORE the drop gates run, preserving the raw audio for a later reconciliation pass rather than losing the words for good' - 'The companion honesty fix: on the first reconnect the client inserts a visible transcript entry reading `[reconnecting — audio may be missing]`, so a gap is named out loud instead of leaving the user to assume the transcript is complete' - 'A durable tee costs disk — the team estimated roughly 86 MB per 30-minute recording — so a `StartupCleanupService` prunes the saved WAVs after 7 days; a safety net that grows without bound is its own bug' - "v1.9.0's self-defeating fix: the client started dropping silence chunks to save bandwidth, which broke utterance finalization because ElevenLabs Scribe's VAD relies on ~1s of silence in the stream to commit a partial — filter the silence out and partials accumulate forever with a trailing '...'" - 'The fix was to re-add the meaning of the silence without the bytes: track sustained silence locally and fire an explicit `commit` after `SILENCE_COMMIT_THRESHOLD_MS = 1000` (matching the backend VAD default) — when you stop sending a signal for efficiency, you may have to send its meaning some other way' --- The last two releases of GeekBye v1 — v1.8.20 and v1.9.0, the ones right before the version-2 milestone — share a theme that only becomes obvious when you read them together. Both are about things the app was quietly throwing away, and both arrive at the same conclusion: the thing you drop to be efficient was doing more work than you thought. One release was dropping audio during network reconnects. The other was dropping silence to save bandwidth. In both cases the dropped thing turned out to be load-bearing. ## Realtime transcription is not lossless, and v1.8.20 admits it Here's a fact about streaming speech-to-text over a WebSocket that's easy to look away from: when the connection drops and reconnects — or when the app fails over from one transcription provider to another, Deepgram to ElevenLabs — the client deliberately drops the audio chunks captured during that gap. It has to. A freshly-connected provider that suddenly receives a backlog of stale audio will choke, or worse, latch its voice-activity detector onto the wrong thing. So the client throws those chunks away. The problem isn't the dropping. The problem is what dropping used to _mean_: those words simply never appeared in the transcript, and nothing told you they were missing. The recording looked complete. It wasn't. v1.8.20 attacks that on two fronts, and neither of them is "make reconnects never lose audio" — because you can't, not in realtime. Instead it makes the loss **recoverable** and **honest**. The recoverable half is a durable **PCM tee**. In the main-process capture handler, every captured chunk is now written to a file on disk _before_ it reaches the reconnect and grace-period drop gates: > every captured chunk is written to `/sessions/session-.wav` before realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass The mechanism is pleasingly low-tech: `teeChunkToDisk` opens a `WriteStream`, reserves a 44-byte WAV header up front, appends raw 16-bit mono samples as they arrive, and patches the header with the final sizes on stop (`fs.openSync(..., 'r+')`, write 44 bytes at offset 0). The result is a playable WAV of the _complete_ audio — including the parts the realtime transcript never saw. When recording ends it emits a `session-audio-saved` event with the file path and byte count. Two honesty notes matter here, because it's easy to overclaim this. First: the tee does **not** fill the gap in these releases. It preserves the raw audio _for_ a later reconciliation pass — a batch re-transcription that reconciles the file against the live transcript — which is a hook these releases lay down, not a feature they ship. The follow-up commit even annotates `session-audio-saved` with a TODO pointing at the reconciliation issues so nobody mistakes it for dead code. Second: this costs disk. The team estimated roughly **86 MB per 30-minute recording**, so the same release adds a `StartupCleanupService` that prunes saved sessions older than **7 days**. A safety net that grows without bound is a leak; the cleanup is what makes the tee shippable. The honest half is even simpler. On the first reconnect attempt, the client writes a real, visible entry into the transcript: > [reconnecting — audio may be missing] That single line is the whole philosophy of the release in miniature. The app can't guarantee it heard everything, so it stops implying that it did. A gap you can see is a gap you can work around; a gap you can't see is a bug report waiting to happen. ## v1.9.0: the silence you optimized away was the signal v1.9.0 is the last v1 release, and it's where the backend handed a set of live-transcript responsibilities down to the client — dropping silence, attributing each transcript line to mic or system audio from a local voice-activity timeline, owning the reconnect loop, enforcing recording caps. Most of that is groundwork for the version-2 architecture and has been told elsewhere. But one thread in it is the best small story in either release, because it's a fix that had to undo the damage of an optimization that was completely correct in isolation. It starts with an obviously-good change: **stop sending silence.** The client began filtering out chunks whose dominant source was silence before shipping them over the socket — less bandwidth, less useless audio for the backend to process, and it honored an explicit backend client-contract that said "don't send silence." Every reason to do it was sound. And it broke utterance finalization completely. The commit that fixes it lays out the causal chain with unusual candor: > After the client began filtering silence chunks … ElevenLabs Scribe's VAD-based commit strategy no longer fires — EL needs ~1s of silence in the audio stream to commit a partial, and that silence never reaches it. Result: partials accumulate forever, UI shows trailing "...", utterances never finalize. This is the platonic form of a leaky abstraction biting back. The transcriber's voice-activity detector doesn't have a separate "the speaker is done" input — it _infers_ the end of a sentence from hearing about a second of silence in the audio. Silence wasn't noise on the wire; it was a message. By filtering it out to save bandwidth, the client accidentally stopped sending the one signal that told the transcriber a sentence had ended. So every partial transcript just kept growing, tail trailing off into a `...` that never resolved. You could fix this by putting the silence back — but then you've un-done the optimization. The actual fix is better: **send the meaning of the silence without sending the silence.** The client now tracks how long the audio has been silent locally, and when that crosses `SILENCE_COMMIT_THRESHOLD_MS = 1000` — deliberately matched to the backend's own VAD silence threshold — it fires an explicit `commit` signal out of band: `sendMixedChunk` notices the sustained silence, sends a `realtime-audio:commit` over IPC, the main process forwards a `{ type: 'commit' }` on the socket, and the backend tells ElevenLabs to finalize the partial. Two small pieces of state, `silenceStartedAt` and `silenceCommitFired`, keep it from firing repeatedly. The bandwidth win survives; the finalization works again; the trailing `...` resolves. There's a small, honest footnote to this release, too: the unbounded reconnect logic that v1.9.0 introduced was first built in the _renderer_ as a `WsReconnectController`, then deleted within the same release once the real implementation landed in the main-process handler where it belonged. The reconnect story itself — unbounded backoff, alternating providers every five failures — is told in its own chapter; what v1.9.0 adds is that it's where that architecture was born, and where the client first took ownership of it. ## Three things these releases taught 1. **When you can't prevent loss, make it recoverable and make it visible.** v1.8.20 doesn't stop reconnects from dropping audio — that's not physically possible in realtime. It keeps a copy of the raw audio for later, and it labels the gap in the transcript. Neither is glamorous; together they turn silent data loss into a known, bounded, recoverable condition. 2. **A safety net needs its own bounds.** The durable tee would quietly fill the disk if left alone — a fix that becomes a slow-motion bug. The 7-day cleanup shipped in the same release is not an afterthought; it's the thing that makes the tee safe to ship at all. If you add a mechanism that accumulates, add the mechanism that prunes it in the same breath. 3. **The thing you drop to be efficient may be a signal.** The single sharpest lesson in either release: filtering out silence was correct on every axis you'd normally check — bandwidth, load, contract compliance — and it still broke the product, because a downstream system was reading meaning into the silence. When you remove data for efficiency, ask what was _inferring_ something from that data. Sometimes you have to send the meaning explicitly precisely because you stopped sending the thing it used to be inferred from. And that's the end of the version-1 story — the last hardening before the rewrite. For the previous chapter, [the three verbs that keep web audio alive](/blog/the-three-verbs-that-keep-web-audio-alive) (v1.8.16 + v2.0.2); for where all of this was heading, [what a version 2 actually takes: 206 commits](/blog/what-a-version-2-actually-takes-206-commits) (v2.0.0); and for the whole arc, [the anatomy of shipping software to perfection](/blog/anatomy-of-perfection).