Reply only when addressed, plus a beta release line - #25
Merged
MyNamesEMurray merged 3 commits intoJul 26, 2026
Conversation
A 2.5h stream on v3.7.0 cost $4 on Opus — about 5x the README's estimate. The per-message price was not the story; the message *count* was. onSpeech treated any speech within 120s of a cast message as an answer to it, but a streamer talks almost continuously — narrating a fight, thinking out loud, talking to a co-op partner. That fired an extra generation after nearly every cast message, roughly doubling spend, and made the cast read as interrupting rather than listening. A voice reply now needs the speech to be addressed: a ghost named out loud, or a prompt answer to a question one just asked. Nothing is lost when it isn't — the transcript window is read by the next scheduled generation either way, so undirected talk still reaches the cast, it just no longer buys its own API call. The question rule is on a deliberately short fuse (voiceAnswerWindowSeconds, 30s): the "curious" archetype ends a lot of messages with "?", so a generous window would match nearly everything and undo the point. Name matching is whole-word, so "wispy" doesn't summon Wisp — and the correction map already works to keep ghost names intact through transcription, which is what makes this signal reliable. cadence.voiceReplyRequiresAddress (default true) restores the old behaviour when false. voiceReplyDelaySeconds and voiceReplyWindowSeconds were hardcoded; both are now config. All hot-appliable. Also surfaces the two numbers that would have diagnosed this in minutes: hover the cost pill for the share of input served from cache and the average output tokens per message (a one-line chat message is ~60; far above that is thinking doing the spending). cache_read_input_tokens was already collected and merged into a single total, which hid it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Test builds can now be produced from any branch without merging and without anyone compiling: Actions -> Beta Release -> pick a branch. It tags vX.Y.Z-beta.N off the newest stable tag, builds the Windows installer, and publishes a GitHub prerelease. Isolation from the stable line is structural rather than procedural: the -beta.N suffix makes electron-builder write the update feed as beta.yml, and a stable install only ever polls latest.yml, so it cannot be offered a test build. Two pre-existing paths had to be fixed first, or the first beta tag would have broken stable releases: - auto-release.yml picked the highest v* tag to bump from. A beta tag sorts ABOVE the release it precedes, and splitting 3.8.0-beta.1 on "." puts "0-beta.1" into $patch — verified to fail with "invalid arithmetic operator", which under Actions' bash -e fails the job. Every stable release would have been blocked until someone deleted the tag. Now filtered to ^v[0-9]+\.[0-9]+\.[0-9]+$. - release.yml triggers on tags v*, which matches beta tags too. Publishing one there would produce a non-prerelease carrying latest.yml — pushing a test build to every stable install. Its job is now guarded against prerelease tags. Beta numbering sorts numerically, so beta.10 follows beta.9 rather than beta.1. Verified against a scratch repo seeded with the real tag history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Correcting something I got wrong when adding the beta line: I described a stable release as superseding its own betas by semver, carrying testers home. It does not. electron-updater follows a channel *file*, not a version comparison — a stable release publishes only latest.yml, so beta.yml goes on advertising the last beta and a tester is never offered the real release. Installing a beta was therefore a one-way door. app.updateChannel (Settings -> App, advanced): - auto (default) — follow whatever build is running. This is exactly the previous behaviour: stable installs read latest.yml, beta builds read beta.yml. Nothing changes for anyone who never touches a beta. - stable — the way back. Forces latest.yml, and is the only case that sets allowDowngrade, because returning from 3.8.0-beta.5 to 3.7.0 is a version decrease and the escape hatch would otherwise silently do nothing. - beta — opt in without hunting for an installer. allowDowngrade is deliberately scoped to "explicitly chose stable while running a prerelease" so a normal install can never be walked backwards. An unrecognised or missing value falls back to auto rather than erroring. Verified the decision matrix across every setting x build combination: allowDowngrade is true in exactly one of them. Not verifiable here — this needs a packaged Windows build to exercise electron-updater for real, which is precisely what the beta line now makes possible. The first beta is the test of the beta machinery. Release-Bump: minor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
MyNamesEMurray
enabled auto-merge (squash)
July 26, 2026 23:51
MyNamesEMurray
deleted the
claude/localvocal-transcription-accuracy-jxxefb
branch
July 26, 2026 23:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cuts the dominant cost driver — the cast was treating almost everything the streamer said as a question aimed at it — and adds a beta release line so changes like this can be tested on a real stream without merging or compiling anything.
Changes
Voice replies only when the cast is actually addressed
A 2.5h stream on v3.7.0 cost $4 on Opus, ~5x the README's estimate. The per-message price wasn't the story; the message count was.
onSpeechtreated any speech within 120s of a cast message as an answer to it — but a streamer talks almost continuously, so an extra generation fired after nearly every cast message.voiceAnswerWindowSeconds, 30s): the "curious" archetype ends a lot of messages with "?", so a generous window would match nearly everything and undo the point.cadence.voiceReplyRequiresAddress: falserestores the old behaviour.voiceReplyDelaySeconds/voiceReplyWindowSecondswere hardcoded and are now config. All hot-appliable.Cost visibility — hover the cost pill for the share of input served from cache and the average output tokens per message (a one-line message is ~60; far above that means thinking is doing the spending).
cache_read_input_tokenswas already collected and then merged into a single total, which hid exactly the number needed to diagnose the above.Beta release line — Actions → Beta Release → pick a branch. Tags
vX.Y.Z-beta.Noff the newest stable tag, builds the Windows installer, publishes a GitHub prerelease. Stable installs can't see it: the-beta.Nsuffix makes electron-builder writebeta.yml, and a stable install only pollslatest.yml.app.updateChannel(auto|stable|beta) — because a beta install does not find its own way back. electron-updater follows a channel file, not a version comparison, so a stable release (which publishes onlylatest.yml) is never offered to a beta install.stableis the deliberate way home, and the only case that setsallowDowngrade.Verification
npm ci --ignore-scripts: syntax check, config validation, 95 tests pass (1 pre-existing Windows-only skip),npm run smokeOK.beta.10followsbeta.9(numeric sort), and a beta tag never becomes the base for the next one.allowDowngradeis true in exactly one (explicitly chose stable while on a prerelease).Risk & rollout
auto-release.ymlpicked the highestv*tag to bump from; a beta tag sorts above the release it precedes, and splitting3.8.0-beta.1on.puts0-beta.1into$patch. Verified to fail withinvalid arithmetic operator, which under Actions'bash -efails the job — every stable release blocked until someone deleted the tag. Now filtered to^v[0-9]+\.[0-9]+\.[0-9]+$. Separately,release.yml'sv*tag trigger also matches beta tags, and publishing one there would emitlatest.yml— pushing a test build to every install. Its job is now guarded.voiceReplyRequiresAddress: falserestores the old behaviour immediately (no restart).cadence.voiceReply*,cadence.voiceAnswerWindowSeconds,app.updateChannel. README rows added.public/overlay.htmlis not modified; the 🤖 AI badge and "simulated viewers" watermark are unchanged.Release-Bump: minor→ v3.7.0 would tag v3.8.0.Checklist
npm run smokepasses locally (or CI is green)main🤖 Generated with Claude Code
https://claude.ai/code/session_01SK871Ay2eiEzxTPorRufLP
Generated by Claude Code