Files
LlamaCasty/HANDOFF.md
T
gramps eb0dd91126 docs: OAuth verification is PARKED, not the next step — the map was lying
The 2026-09-27 entry sat under a "NEXT STEP" heading and framed the Google
verification as the top priority. It was neither un-attempted nor un-blocked:
the attempt had already failed on a Google-side bug and the creator had
already ruled it unimportant for where we sit. A "next step" heading lends a
dead end borrowed authority, and the next session would have opened the
console instead of the editor.

Records the ruling in the terms that change behaviour: attempted, died on
Google's side, ruled unimportant, do not re-attempt. States the cost of
tabling it honestly — an accepted 7-day refresh-token expiry while the
consent screen sits in Testing, which is why the creator kept having to
re-consent. The sensitive-vs-restricted classification was never read and is
moot until a second real user or Store submission.

The mechanism of the Google-side bug was never recorded and is left
deliberately blank rather than reconstructed — a confident fiction in a
memory map is the one sentence the next session would trust. The project
number and console link stay in ai.md:233; they cost a hunt and are still true.

The two code scope cleanups survive the tabling (neither needs verification
to land): drop the redundant force-ssl scope, and move the uploads-playlist
fetch off the user's session token. The channels?mine=true call on the same
service legitimately needs the session — flagged so it is not "fixed" later.

No code touched. Test state unchanged.
2026-09-28 09:25:27 -07:00

15 KiB
Raw Blame History

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:

  1. The sign-out ran first. ChangeAccountAsync was await SignOutYouTubeAsync(); await SignInAsync();. SignOutYouTubeAsync → TokenStore.Clear() → deletes ytLlive.auth. So the working account's token was destroyed before the browser even opened; cancelling the chooser left nothing. Now ChangeAccountAsync is just await SignInAsync() — success overwrites the token via the sessionChanged hook, cancel/failure keeps the current session. No sign-out is needed at all.
  2. The retry couldn't rebind the callback port. Abandoning the browser gives the app no signal, so the attempt held localhost:8765 for its full 5-minute timeout. A new AuthenticateAsync now cancels and awaits the abandoned attempt before binding, serialised by a SemaphoreSlim (_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.

These two survive the tabling — neither needs verification to land, and both shrink the surface if it ever comes back:

  • Services/YouTubeAuthService.cs:18 requests auth/youtube + auth/youtube.force-ssl. force-ssl is redundant — Google's liveChat docs list auth/youtube as an acceptable alternative, and it is the broader of the two ("Manage your YouTube account" ⊃ "see/edit/delete your videos, ratings, comments, captions"). Drop it: one fewer scope to justify, one fewer consent line.
  • Services/ChannelStatsService.cs:104,107 — playlistItems.list on the creator's own uploads playlist (UU…) is public data needing no OAuth scope at all (API key suffices); it currently rides on the user's session token. Does not change the requested scope (auth/youtube is forced by liveBroadcasts.*/liveStreams.list regardless) but that fetch should not need user credentials. The channels?mine=true call on the same service does need the session — do not "fix" it.
  • auth/youtube is unavoidable: no narrower scope covers live broadcast create/transition.

✅ 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.

The first code unit: bundle ffmpeg (TASK 48 item 1)

Two real defects the research surfaced — neither is fixed yet, this was a docs unit:

  1. ⛔ FfmpegLocator downloads an unsigned exe from GitHub and runs it. Store policy 10.2.2 (dynamic code inclusion) verbatim, and the root cause of the 2026-09-01 failure — the pin aged out of BtbN's 14-day retention and the download 404'd on the creator's first real recording attempt. FfmpegLocator.cs:30-35 records the incident but still reads like a design choice. Fix: bundle it. Also deletes the startup network dependency.
  2. ⛔ Distribution.md:318 recommended a $400+/yr EV certificate for a SmartScreen bypass Microsoft removed in March 2024. Fixed last commit — 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 in MyMistakes.md stands: a policy citation is a claim about scope, not just text.

⛔ Two invariants that break silently if touched

  • Full trust, or recording breaks. DefaultRecordFolder() writes to Downloads/MyVideos; that passes through unvirtualized only at mediumIL. Flip the manifest to appContainer and 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 and task-36-gold-pass.md still shows items 1–6 unchecked. The line most likely means item 2 (branding flash goes live) shipped at 9761b1d. 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) locked bin/.../ytLive.exe and broke dotnet run with MSB3027. Killed. If the build fails to copy ytLive.exe, check for a running instance first.
  • LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder is flaky by environment (needs an interactive desktop session; 2 fail / 1 pass in isolation). Run the suite with ytLive CLOSED. Not a regression.
  • RealAppHost.RunAsync exists for a reason — a frame-pump test MUST await inside it. A blocking wait occupies the one shared STA thread and hangs the whole suite with no output. Always background a vstest run and poll the log; a foreground piped vstest returns 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

367/367 as of 938c5b3. Not re-run for this unit — docs only, no code touched.

Uncommitted / untracked

  • 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 for CREDENTIALS.md — never commit those.

Next

  1. Bundle ffmpeg (TASK 48 item 1) — unblocked, first code unit, fixes a real user-facing failure and clears the one hard certification gate.
  2. Settle the price — one-time vs subscription, and the number. Then declare the IAP products in Partner Center. Blocks: the Store listing, and the OverlayHost.xaml copy.
  3. 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.
  4. Polar teardown (TASK 48 item 0) — PolarLicenseService, PolarLicense, MainViewModel.License.cs, the OverlayHost.xaml string, the csproj/App.xaml.cs Velopack sites. IsPremium ← Store entitlement. Do this as one unit — a half-torn-down licensing path is worse than either end state.
  5. Verify the Store revenue-share rate — before step 2, not after.
  6. 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:645 flags off-size tiers as a known follow-up.
  7. 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.
  8. Post-session efficacy report — roll up CurrentHealth (dropped frames, duration, health message) when a stream or recording ends. SessionTeardownTests is the natural home.
  9. Measure whether the Windows camera toggle gates DShow — gates all privacy-forward copy (MARCOM.md guard). Not a certification risk; a trust risk.
  10. 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 makes it disappear), 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 4).

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".