The handoff asked for the RTMP diagnostic as item 1 and claimed the next code unit was packaging. Both are now wrong, and per the map's own rule a stale handoff is worse than none because the reader trusts it. 1. DIAGNOSTIC ANSWERED, and it narrowed the question rather than closing it. ffmpeg reaches YouTube — rtmp://a.rtmp.youtube.com/live2/..., 7.4 MB video plus 46 KB audio muxed, no socket error. The broadcast never leaves `ready`: enableAutoStart=true was sent, yet close-out refused `complete` with 403 invalidTransition, which is only legal from `live`. Two things stay honestly unknown — the push was only 8s, and the app never polls broadcast status during a stream (only stream health), so the log cannot show whether or when it flipped. Next step is one log line plus one 60-second test. 2. Packaging is DEFERRED, not next. Creator ruling 2026-10-04: still in active development, so ship-time work waits. Item 1 of "Next" said the opposite. 3. Push state: origin/main =cc2d1efas of 2026-10-04, tree clean. Stale push state has now bitten twice here, so the line now tells the next session to verify with rev-list instead of believing the file. 4. Test count 381, not 380. 5. New landmine: the frame pump logged 86 stalls per 5s (142 lines in an 8-second window) while stats still read 301/300 frames and 0 dropped. Those two facts contradict each other; recorded as a measurement to take, not a bug to fix from the log. Startup stall (TRAX / unreachable NAS) shipped incc2d1efand is documented in ai.md; not duplicated here.
24 KiB
HANDOFF — current state
Branch: main (pre-1.0, no feature branches — creator ruling 2026-08-24).
Pushed 2026-10-04: origin/main = cc2d1ef, working tree clean. That push carried
13 commits (the 2026-09-27 → 10-02 backlog plus two units from today). The stale-push-state
failure MyMistakes.md warns about has now bitten twice, so the rule stands: check
git rev-list --count origin/main..main before trusting any line here about push state.
🚨 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 at that point (380 now — see Test
state); 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.
Deferred, not next: the packaging project + Package.appxmanifest (TASK 48 items 2–3)
Creator ruling 2026-10-04: still in active development, so ship-time work waits and
queued features come first. The unit is unchanged and ready when it is wanted:
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.
🚨 OPEN 2026-09-28 — the RTMP push never lands: recordings reach YouTube as EMPTY placeholders
This is a live bug, not a queued feature, and it reframes a complaint the map previously treated as closed. It was found on 2026-09-28 while scoping TASK 50, and the handoff had no entry for it at all — corrected here.
What the creator observed: the channel carries 20+ private videos, all content-less placeholders (creator-inspected, 2026-09-28). This is the real explanation of the 2026-09-26 report "I still cannot get private test recordings saved" — it was never true that the app could not find an archived VOD. No video content is ever produced on the channel.
Mechanism (verified against the code AND the channel): liveBroadcasts.insert succeeds and
creates the placeholder; everything after it is what fails. Content only exists if all three of
these land, and at least one does not:
- broadcast reaches LIVE via
liveBroadcasts/transition, - ffmpeg actually pushes RTMP to the bound stream key,
- the broadcast ends and YouTube transcodes it into a VOD.
enableAutomaticArchiving is never set, so YouTube's true default applies — the archiving
route exists but has nothing to archive. Broadcasts are inserted private
(Services/YouTubeStreamService.cs:49), so the orphans are not public, and the app has no
delete path — the creator clears each one by hand in Studio.
✅ DIAGNOSTIC ANSWERED 2026-10-04 — ffmpeg reaches YouTube; the broadcast never goes LIVE.
One test-mode push, read from startup.log. The three steps, resolved:
new MediaFoundationReader/RTMP path — WORKS. ffmpeg openedrtmp://a.rtmp.youtube.com/live2/4pew-…, muxed 7.4 MB video + 46 KB audio, and finalised the FLV with no socket error. Step 2 is fine.- The broadcast stays
ready— this is the break.enableAutoStart = trueis sent on insert (Services/YouTubeStreamService.cs:73), yet at close-out YouTube refusedtransition=completewith 403Invalid transition— which is only legal fromlive. So YouTube never flippedready → live. - Never reached (nothing to archive because nothing went live).
Two unknowns, and the honest state of each:
- The push ran only 8 seconds (08:48:23.9 → 08:48:32.8). YouTube may simply need longer to accept and validate the ingest before it auto-starts. Untested — do not assume.
- The app cannot see this either way. During a stream it polls stream health
(
liveStreams) and never the broadcast status;GetLifeCycleStatusAsyncis called only from the end path (YouTubeStreamService.cs:146). So the log contains no evidence either way, andEndBroadcastAsyncskips only oncomplete/revoked, meaning it triescompleteagainst areadybroadcast and earns the 403.
⇒ Next step, small and cheap: log the broadcast's lifeCycleStatus while streaming
(reuse the existing health poll timer — no new loop), then run one 60-second test. That
single run answers it. Do not go hunting beyond it until it has.
Sequencing constraint — do not flip the flag early. enableAutomaticArchiving: false is the
correct end state (TASK 50 will upload the local file deliberately), but archiving is
currently the only route a recording takes to YouTube — flipping it before TASK 50 ships
would leave test streams with nothing at all and re-create the 2026-09-26 complaint in a worse
form. Ship the upload, flip the flag in the same task, never before.
Detail and the open 11.12 re-check: TASKS/task-50-publish-recording.md.
Landmines
- Frame pump jitter, unquantified (2026-10-04). The same test stream logged 86 stalls
per 5s (
FramePump stall: iteration 40–50msagainst a 17ms budget; 142 such lines in an 8-second window) whileFramePump statsstill read 301/300 frames, 0 dropped. Nominal 60fps with zero drops and repeated 3× overruns is a contradiction worth one measurement, not a guess. Do not "fix" it from the log alone. - 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
381/381, 0 warnings, re-verified 2026-10-04 on a clean test-project build (380 + the TRAX probe test added today). Build the test project, not just the app project, or VSTest runs a stale DLL.
⚠️ New flake, do not chase it as a regression: on the first full-suite run,
AudioPipelineTests.MasterLimiter_CapsTheLiveMix_OnThePipe failed with Expected 0.891, Actual 0 — a zero, not a slightly-off value, which is the signature of the pipe not being
fed yet rather than the limiter misbehaving. It passed 27/27 as a class run and 380/380 on an
immediate full-suite re-run. It reads the live mix off the named pipe, so it is a
whole-suite scheduling artifact of the same family as the RealAppHost STA landmine below —
if it ever goes red, re-run before believing it.
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
- Log the broadcast status during a stream, then one 60-second test — the RTMP diagnostic came back "ffmpeg fine, broadcast never goes live", which is a narrower question than the one item 1 used to ask. One code change (reuse the health poll timer) plus one long test closes it. It is the live bug, so it still sorts first; nothing below reorders. Packaging is NOT next — the creator's ruling 2026-10-04: we are still in active development, so ship-time work waits and queued features come first.
- 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. - TASK 50 — one-button "Publish this video" (
TASKS/task-50-publish-recording.md). Queued behind the Store submission by its own precedence note ("MSIX first, publish second — unless the creator reverses that"). Scoping is done; the build is not. ⛔ Do not flipenableAutomaticArchivinguntil it ships. It also carries an open 11.12 re-check: a publish feature makes the app push creator content to a third party, so the IARC answers must not go to certification describing an app that cannot publish. - 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".