force-ssl is gone from the requested scope. Its grant ("see, edit, and
permanently delete your videos, ratings, comments and captions") is a strict
subset of auth/youtube's ("Manage your YouTube account"), and Google's own
liveBroadcasts.insert reference lists auth/youtube as an accepted ALTERNATIVE
to force-ssl, so every endpoint this app calls — channels, liveBroadcasts,
liveStreams, liveChat, playlistItems — is covered by the one we keep. Two
consent lines became one, and one fewer scope to justify. Removing a scope
cannot narrow capability when the scope being kept is the broader of the two,
and there is no forced re-consent: a refresh token keeps the scopes it was
originally granted, so existing sessions are untouched.
The playlistItems change is NOT being made, and the map entry recommending it
was wrong. It came out of the verification hunt on the theory that public data
should not ride on user credentials. Walking the code kills it: ChannelStats-
Service is built with one HttpClient, and channels?mine=true on the same
service genuinely needs the session. Splitting it means a second unauthenticated
client PLUS a Google API key, since every YouTube Data API request needs a
credential and the call 403s without one. So the fix would ship a new
extractable, quota-abusable credential inside the binary to replace a token the
app already holds, buying zero verification-surface reduction (the data is
public, no scope involved) and zero correctness. Strictly worse. Recorded as a
do-not-do so it is not "helpfully" reintroduced later.
Test: ExactlyOneScopeIsRequested_AndItIsTheBroadOne asserts on the const's
parsed VALUE rather than scanning the file — a file-wide DoesNotContain
fails on the doc comment that names force-ssl to explain its removal, which is
the same trap I hit and documented earlier today in the locator test. Suite
380/380, 0 warnings.
18 KiB
HANDOFF — current state
Branch: main (pre-1.0, no feature branches — creator ruling 2026-08-24).
Last pushed: e7cded6 (pricing/Store fee docs). Local, unpushed: 536676f (recording
filename ms) and the Change Account fix in this unit — ask before pushing.
🚨 FIXED 2026-09-27 — "Change Account" no longer signs you out before the browser opens
Reported as: "I clicked on my signed-in account pic, then clicked sign-in to another account. It opened the browser page to login under a different account and I canceled the request. But when returned to the app, I see logged out — and should not have happened! Cannot sign-in because the callback url was not released." Also reported as hitting both running instances.
Two independent defects, and the second one was masking a race:
- The sign-out ran first.
ChangeAccountAsyncwasawait SignOutYouTubeAsync(); await SignInAsync();.SignOutYouTubeAsync→TokenStore.Clear()→ deletesytLlive.auth. So the working account's token was destroyed before the browser even opened; cancelling the chooser left nothing. NowChangeAccountAsyncis justawait SignInAsync()— success overwrites the token via thesessionChangedhook, cancel/failure keeps the current session. No sign-out is needed at all. - The retry couldn't rebind the callback port. Abandoning the browser gives the app no signal, so
the attempt held
localhost:8765for its full 5-minute timeout. A newAuthenticateAsyncnow cancels and awaits the abandoned attempt before binding, serialised by aSemaphoreSlim(_authGate) — without the gate, the takeover window let a third caller see a free port and race to bind it, i.e. the takeover reproduced the original failure. My own test caught this.
The cross-instance claim was NOT the cause and is still open as a separate question: no code path
can sign out a second instance (TokenStore is per-InstanceProfile.DataRoot, Clear() is a local
file delete with no server-side revoke, and no second instance had a token at inspection time). Verify
both processes carried distinct YTLIVE_INSTANCE values and ran Debug builds before chasing it.
Test: YouTubeAuthServiceTests.RetryingAfterAnAbandonedAttempt_ReleasesTheCallbackPort. Full suite
370/370, 0 warnings (build ytLive.Tests.csproj, not just the app project, or VSTest runs a stale
test DLL). Added an openBrowser ctor seam to YouTubeAuthService so tests never launch a real
browser. Coverage gap: the ordering fix (defect 1) has no automated test — _youtubeAuth is
hardcoded in MainViewModel and ChangeAccountAsync is private, so a real test would shell out to
the browser. Wiring an auth seam into MainViewModel is a deliberate follow-up, not a bug-fix extra.
🗄 PARKED — Google OAuth verification: attempted, hit a Google-side bug, tabled by ruling
Creator ruling: unimportant for where we currently sit. Verification is not a live blocker and
must not be re-attempted, re-derived, or re-researched without a new ruling. The 2026-09-27 entry
that preceded this one wrongly sat at ⏭ NEXT STEP and framed it as the top priority — that was the
plan going in, not the outcome, and the outcome was never written down. Corrected 2026-09-28.
What happened: the verification attempt was made and failed on a bug on Google's side — not something we can fix, work around, or retry our way past. ⛔ The exact mechanism was never recorded and is deliberately left blank rather than reconstructed from memory: a confidently wrong note here is worse than none, because the next session would trust it. Do not go looking for the cause.
Owning account (creator-confirmed): llamachile.shop@gmail.com ("gramps"). Cloud project number
198374416003 (from the ClientId) — banked in ai.md:233, do not re-hunt it. The one-hop console
link is https://console.cloud.google.com/apis/credentials?project=198374416003 (picker defaults to
"Recent" — switch to "All"). ⛔ Not the same account as the YouTube channel signed into the app.
What tabling costs, stated honestly: if the consent screen is still in Testing, the 100-user cap and the 7-day refresh-token expiry both stand — that expiry is why the creator kept having to re-consent. Parking verification does not fix that; it accepts it for the solo-use phase. It becomes real work the moment a second real user needs to sign in, or at Store submission (TASK 48). The sensitive-vs-restricted classification was never read and is moot until then.
One of these two survives the tabling, and the other was a bad idea (corrected 2026-09-28):
- ✅ DONE —
Services/YouTubeAuthService.csno longer requestsauth/youtube.force-ssl. It is redundant: its grant is "see, edit, and permanently delete your YouTube videos, ratings, comments and captions", a strict subset ofauth/youtube's "Manage your YouTube account", which is the scope we keep. Google's ownliveBroadcasts.insertreference listsauth/youtubeas an accepted alternative toforce-ssl, so every endpoint this app calls (channels,liveBroadcasts,liveStreams,liveChat,playlistItems) is covered. Two consent lines became one. No forced re-consent — a refresh token keeps the scopes it was originally granted, so existing sessions are untouched; the reduced set applies at the next fresh sign-in. Removing a scope cannot narrow capability when the one kept is broader. - ❌ NOT doing this, and the earlier note recommending it was wrong: moving
playlistItems.listoff the user's session token. It was written during the verification hunt on the theory that "public data should not ride on user credentials". Walking the code kills it:ChannelStatsServiceis built asnew ChannelStatsService(_youtubeAuth)with oneHttpClient, andchannels?mine=true(same service) genuinely needs the session. To split it you would need a second unauthenticated client plus a Google API key, because every YouTube Data API request needs a credential — without a key the call 403s. So the "fix" means shipping a new extractable credential in the binary, subject to quota abuse, to replace a token the app already holds. It buys zero verification-surface reduction (the data is public; no scope is involved) and zero correctness. Strictly worse. ⛔ Do not "fix" this later without a new ruling.
✅ DECIDED 2026-09-27 — pricing: $10/mo subscription, or $400 lifetime, no annual
**Creator's ruling, verbatim: "Let's just go with $10 monthly recurring subscription for option
A and, for option B, a lifetime sub for $400. No annual." ⛔ No .99 prices (earlier
ruling: "Let's stop with the x.99 stuff - I find that irritating. Just say: ten bucks a
month"). This supersedes the 2026-09-21 one-time $29 → $49 model and the old $99/yr
sub. No feature gating: free gets everything, the branding flash is the only paid delta.
✅ Store revenue share VERIFIED — 15%, and subscriptions are NOT penalised. Read from the ADA v8.10 PDF itself (downloaded, text-extracted — not a search excerpt). §6(b)(i) charges 15% on "any Apps (and any In-App Products in such Apps) that are not listed in Section 6(b)(iii)"; 12% is Games-only; 30% covers Xbox console apps/games, Xbox non-subscription IAP, and Windows 8/Phone 8. LlamaCasty is a Windows PC App — not a Game, not Xbox, not Win8 — so 6(b)(i) governs both tiers. The agreement's own changelog (v8.0, Oct 26 2017) says it outright: "implement the 85/15 revenue share for non-Game subscriptions."
⇒ $10/mo nets $8.50/mo (~$102/yr). $400 lifetime nets $340. Also confirmed: the
15% applies after VAT/GST (Net Receipts), payouts are monthly above a $50 threshold, and
no better small-indie rate was found in the standard terms.
⛔ Two obligations §6(h) attaches to the recurring tier (why lifetime is the hedge):
(1) we must fulfil the subscription for the entire period as marketed, and on breach
Microsoft may refund the customer "the full amount, plus taxes... in Microsoft's sole
discretion"; (2) raising the price disables auto-renew — so $10 is effectively locked
for the product's life absent Microsoft's price-change process.
⏳ Still open: the $50 subscriber→lifetime upgrade-discount SKU was discussed and is
not approved — do not declare it. Individual vs Company Partner Center account still
unanswered (get it right before enrolling).
✅ DECIDED 2026-09-27 — distribution is the Microsoft Store: MSIX + Store IAP
Creator's criteria, verbatim: "zero headaches, minimal maintenance (for me) while still
providing accountability and a reasonably easy upgrade flow." Route A is the only option
where all four are solved by handing the work to Microsoft rather than to a certificate
vendor: $0/yr, no certificate, no HSM, no annual renewal, no SmartScreen ramp, plus Store
auto-update, Store-side payments/entitlements/refunds/support, and Microsoft review as the
accountability layer. The rejected options and their reasons are in
TASKS/research-store-certification.md §3 — read that before reopening the question, not
before re-deriving it.
The big consequence: the whole licensing backend is deleted. PolarLicenseService,
PolarLicense, MainViewModel.License.cs (PremiumUrl, customer portal, the
OfflineGracePeriod = 14 days subscription-era artifact, renewal/lapse copy) and the wrong
"Polar unlocks alerts" string all become dead code. IsPremium comes from the Store
entitlement instead of an HTTP call — which also deletes the entire "network flaky → app
thinks I'm expired" bug class. Velopack, the update URL and the DO droplet go too.
What does not change: the entitlement. Free gets everything; the branding flash stays the
ONLY paid delta. Store IAP changes how IsPremium is obtained, never what it gates.
⛔ The old "still open: the price" note is retired — the price is settled, see the top of this file. The old "~$69 floor" and "don't break the launch-price promise" notes refer to the old Polar storefront and are not current.
✅ DONE 2026-09-28 — TASK 48 item 1: bundle ffmpeg (was the "first code unit")
The app no longer downloads an executable at runtime. scripts/fetch-ffmpeg.sh populates a
gitignored ffmpeg/ from a SHA-256-verified pinned month-end BtbN artifact; ytLive.csproj
copies it to output and publish and fails the build if it's missing; FfmpegLocator lost its
downloader, its zip extraction and the %APPDATA%\ytLlive\tools cache, and now resolves
bundle → PATH with a loud, actionable failure. The pin that 404'd is gone, so the
2026-09-01 failure class cannot recur. Suite 379/379, 0 warnings; 16 locator facts including an
integration test that executes the bundled binary (which is what proves the sibling DLLs
resolve). THIRD-PARTY-NOTICES.txt corrected — it still named the stale tag
autobuild-2026-08-09-13-03, i.e. the one that 404'd.
Fresh-clone cost, deliberately accepted: ./scripts/fetch-ffmpeg.sh once (~65 MB). 135 MB of
DLLs in git would tax every commit on this 9p tree; the URL + SHA-256 are the audit trail.
Answering "other 3p binaries needing bundling?" — no, verified. ffmpeg was the only
download-and-execute. e_sqlite3.dll + WebView2Loader.dll already ship via NuGet
(runtimes\win-x64\native\), OpenH264 is compiled into avcodec-63.dll, the rest are managed.
Two gaps remain that bundling cannot fix, and they are the next real risks: the WebView2 Evergreen
runtime is a machine prerequisite (item 2's manifest dependency + a graceful-failure path), and
Velopack's updater downloads and executes, so its teardown in step 3 is a compliance action.
The next code unit: the packaging project + Package.appxmanifest (TASK 48 items 2–3)
Full trust, webcam/microphone, English only; Velopack deleted. Add the DefaultRecordFolder()
comment in the same unit (the mediumIL invariant below becomes load-bearing the moment the manifest
lands). Full-trust writes under a real package identity are still unverified — confirm on a
real packaged build before trusting them with recordings.
Carried over from the research unit, still open:
- ⛔
Distribution.md:318recommended a $400+/yr EV certificate for a SmartScreen bypass Microsoft removed in March 2024. Fixed then — had it shipped, it would have cost $400+/yr to buy what $150 buys. Now moot anyway (MSIX is signed by Microsoft), but the lesson inMyMistakes.mdstands: a policy citation is a claim about scope, not just text. - WebView2 Evergreen runtime is a machine prerequisite the app will hard-fail without. The manifest needs the framework dependency, and WebView2 init needs a graceful error instead of a crash. New, found while bundling ffmpeg.
⛔ Two invariants that break silently if touched
- Full trust, or recording breaks.
DefaultRecordFolder()writes to Downloads/MyVideos; that passes through unvirtualized only atmediumIL. Flip the manifest toappContainerand output vanishes with no error. A comment is owed there when the manifest lands (TASK 48 item 6) — not before. - Chat is rendered, never stored. That non-persistence half is the load-bearing part of the 11.12 certification answer. No chat history, moderation log, analytics, or crash payload may be added without re-arguing 11.12 first.
Landmines
- Stale fact inside
TASKS.md(left alone, flagged here per the Scope Lock). The "1.0 gates" section asserts "TASK 36 is now shipped", but the catalog row 36 reads ☐ Queued andtask-36-gold-pass.mdstill shows items 1–6 unchecked. The line most likely means item 2 (branding flash goes live) shipped at9761b1d. Needs a one-line fix, not a code change — flagging rather than fixing because it is not this unit's job. - A stale
ytLive.exe(PID 2544) lockedbin/.../ytLive.exeand brokedotnet runwith MSB3027. Killed. If the build fails to copyytLive.exe, check for a running instance first. LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorderis flaky by environment (needs an interactive desktop session; 2 fail / 1 pass in isolation). Run the suite with ytLive CLOSED. Not a regression.RealAppHost.RunAsyncexists for a reason — a frame-pump test MUSTawaitinside it. A blocking wait occupies the one shared STA thread and hangs the whole suite with no output. Always background avstestrun and poll the log; a foreground pipedvstestreturns nothing in this shell even on success.GlobalHotkeys: two instances registering the SAME global hotkey — Windows refuses the second. Left alone deliberately.- MSIX writes under a real package identity are unverified. The docs say full trust passes user-profile writes through, but confirm on a real packaged build before trusting it with recordings (TASK 48 item 6).
Test state
379/379, 0 warnings, as of the ffmpeg-bundling unit (2026-09-28). Build the test project, not just the app project, or VSTest runs a stale DLL.
Uncommitted / untracked
ffmpeg/— the 9 bundled binaries (~135 MB), gitignored by design. Populated byscripts/fetch-ffmpeg.sh. Do not commit it.MARCOM.md,MONETIZATION.md— both edited (privacy-copy guard; the Polar-obsolete banner and the re-opened-price note) and both gitignored by design (confidential business files,.gitignore:10-11). They stay local, as intended. Same forCREDENTIALS.md— never commit those.
Next
- The packaging project +
Package.appxmanifest(TASK 48 items 2–3) — the next code unit. Full trust,webcam/microphone, English only; Velopack deleted. Add theDefaultRecordFolder()comment in the same unit, and the WebView2 framework dependency + graceful-failure path. - Declare the IAP products in Partner Center — the price is settled ($10/mo, $400
lifetime, no annual, 15% verified), so this is unblocked. Blocks: the Store listing and the
OverlayHost.xamlcopy. ⛔ The Individual-vs-Company account question is still unanswered — get it right before enrolling. - Polar teardown (TASK 48 item 0) —
PolarLicenseService,PolarLicense,MainViewModel.License.cs, theOverlayHost.xamlstring, thecsproj/App.xaml.csVelopack sites.IsPremium← Store entitlement. Do this as one unit — a half-torn-down licensing path is worse than either end state. - Vertical recording verification — the compositor tier is proven by
Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop; the record path has never been run.FramePump.cs:645flags off-size tiers as a known follow-up. - WACK + certification notes + the privacy policy (TASK 48 items 4–5) — the policy must state camera/mic is OS-gated, nothing is retained, and nothing is fetched at runtime (now literally true — the ffmpeg bundle is what makes that sentence defensible).
- Post-session efficacy report — roll up
CurrentHealth(dropped frames, duration, health message) when a stream or recording ends.SessionTeardownTestsis the natural home. - Measure whether the Windows camera toggle gates DShow — gates all privacy-forward copy
(
MARCOM.mdguard). Not a certification risk; a trust risk. - TASK 36:212 stale "shipped" line — one-line fix, needs a hand.
Dropped from the list because the ruling made them moot: multi-instance's ffmpeg tools-dir
race (item 1 deleted the tools dir entirely), scripts/publish.sh + the LlamaCasty.exe rename
(the Store packages and delivers — revisit only if we ever ship outside the Store), and the
alert-gating copy (the wrong string dies with the Polar teardown in step 3).
Everything else in the v1 queue: stream resilience (32), bandwidth step-down (33), scheduled
streams (34), scene-linked audio (35), the master limiter (12), text source (3.16), reward
events (3.20), the media picker (21), webcam identity (9.5), settings units A/C/D (40), and the
defaults split (37). Ship-checklist items live in TASKS.md -> "1.0 gates".