commit 74cdd1fa3441d613c961e10974159b5d956fdff9
parent 69e9f1b18e0d484986efcd6bf92890dae6cfec2f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 11:35:22 -0400
plans: slice XL as shipped — the Connect window without automation signals, the browser-login source, what was measured; FACTS; the editor changelog
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 235 insertions(+), 1 deletion(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -14,6 +14,7 @@
- **A job cancelled before it started now stays cancelled.** Its record on disk kept saying "queued", so a restart could put a job you had just cancelled back in its queue, and a clip fetch cancelled while waiting could be reported as still queued. Jobs still waiting when the editor shuts down are handled as before: the next start settles or re-queues them.
- **A site's Charts tab gives a sixth series its own colour.** The dashboard's charts coloured their series from five colours and started again at the sixth, so a chart broken down by six or more channels drew the sixth in the first one's colour. The sixth now takes the palette's sixth colour, and each from the seventh on a hue of its own; the first five are unchanged. The published sites' charts get the same change with their next build.
- **A form whose save is refused keeps what you typed.** Every editor form put its plain fields back to the stored values when its save was refused — a site's ID rejected, a page size out of range, a slug already taken — so everything typed had to be typed again. A refused save now leaves every field as you left it, beside the reason: **Settings**; a site's form (new and existing); the hub's config on `/sites`; **Cut release**; a channel's form (new and **Configure**), **Rename** and **Delete**; a video's **Delete directory**; **Drive health timing** on `/storage`; the backup config on `/saved-videos`; the sync operation's controls; the **Digest**, **Diarization**, **Speaker attribution** and **Speaker work lane** settings; and the worker list on `/workers`. A save that succeeds behaves as before, with one difference you may notice: a drop-down, and a checkbox or choice that the page tracks as you change it (a cadence, a worker's **Enabled**, a social link's **Keep in header**, a site membership, a site's accent), now shows what was saved. A form's own drop-downs used to go back to what the page had loaded with until a reload, and a second save from the same page sent that old choice again; the others went back until the page next refreshed itself (every 5 seconds by default).
+- **Connecting an X account opens your own browser, and the X fetchers can use your everyday browser's X login instead.** **Settings → X account session → Connect X account** used to open Playwright's bundled Chromium with its automation signals on (the "controlled by automated test software" bar, `navigator.webdriver`): Google's sign-in refused it and X's own login form stalled in it. It now opens your Chromium or Chrome when one is installed (`ARCHILYZER_X_BROWSER` names another; Playwright's bundled Chromium otherwise), without those signals. Google's sign-in may still refuse an embedded browser; X's password login is the reliable path. A new **Login source** choice (`social.x.cookieSource` in `settings.json`) says where the X fetchers' login comes from: **Browser login** hands gallery-dl `--cookies-from-browser` with your `cookiesFromBrowser` on every fetch, so the login lasts as long as you stay logged in to x.com in that browser and no window is needed; **Connected profile** is the session broker, as before. Left on **Automatic**, it is the browser login when `cookiesFromBrowser` is set and no profile is connected, and the profile otherwise. **Check** says which source is in use, whether an X login is visible in it and when it was last used (the browser's cookies are read from a private copy, never written; this reads Firefox's, and gallery-dl reads Chromium's itself). Needs a rebuild and restart of the editor.
## [0.11.0] - 2026-09-30
- **Transcripts that arrived after a video was first seen are counted.** The stats behind the homepage, the hub and every site's charts were cached per video and refreshed only when the video's metadata changed, so a transcript that came later — a Whisper run days after the download, or a video downloaded after the last index build — never reached them, and a video with YouTube captions alone had no transcription date. Counts and charts were low; the homepage could show a site with 0 transcripts, 0 channels and 0 hours while it served its videos. A stat is now also redone whenever the index re-reads the video, every transcript has a date, and a captioned video is dated by when its captions arrived rather than by a later Normalize run, so its place on "Transcribed over time" can move. **After updating, rebuild and restart the editor before anything else:** until then, **Build stats dataset** runs the old code and would undo the new stats, while a site, hub or homepage build already runs the new code — and the first stats build of any kind re-reads every video once (about 10–30 minutes on a large archive; it can be stopped and picks up where it stopped). Then build the index, the stats, the homepage, the hub, and the sites.
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -8090,3 +8090,73 @@ source mirror (homepage)". Anchors are at the branch.
(a read never throws). `forms-keep-input.spec.ts` `withSettingsUnwritable` restores the file in a
`finally`. A background writer in that window fails the same way, so the spec keeps it to one
submit.
+
+## Connecting an X account (verified 2026-10-01, branch `r16/x-login`)
+
+- **Why Google refused the Connect window: the automation signals.** Measured on this box under
+ Xvfb (headed, no window on the operator's display; the slice's `xl-exp/`), Playwright 1.59.1
+ driving system Chromium 153 and the bundled build:
+ - Playwright's defaults: `navigator.webdriver === true` and the bar "Chrome is being controlled
+ by automated test software" (that is `--enable-automation`).
+ - `ignoreDefaultArgs: ["--enable-automation"]` alone: the bar goes, **`navigator.webdriver` stays
+ true** — Playwright's `--remote-debugging-pipe` sets it. `--disable-blink-features=
+ AutomationControlled` is what turns it off (false, headed and headless).
+ - A system Chrome then draws "You are using an unsupported command-line flag" for
+ `--disable-blink-features=AutomationControlled`, and for Playwright's default `--no-sandbox`.
+ `--test-type` (what ChromeDriver passes) suppresses the first; `chromiumSandbox: true` removes
+ the second (the sandbox starts on this box for both builds). The bundled Chrome for Testing
+ draws neither (it honours Playwright's `--disable-infobars`).
+ - Not hidden by any of this: the debugging pipe itself, and the headless user agent
+ (`HeadlessChrome/…`). Google's sign-in may still refuse; X's password login is the reliable path.
+- **The launch options are one pure builder**, `buildXBrowserLaunchOptions`
+ (`common/social/xBrowser.ts`): `ignoreDefaultArgs: ["--enable-automation"]` ONLY, `args`
+ `--disable-blink-features=AutomationControlled` and `--test-type`, the sandbox on for the headed
+ window (it retries without when the host cannot start one). **Never `ignoreDefaultArgs: true`:**
+ it also drops `--password-store=basic`, and a system Chromium would then encrypt the profile's
+ cookies with the desktop keyring's key, which the bundled headless build that refreshes the jar
+ cannot read.
+- **Which browser opens.** `ARCHILYZER_X_BROWSER` (a path, or a name on PATH; one that is not an
+ executable refuses the connect), else the first of `chromium`, `google-chrome`,
+ `google-chrome-stable`, `chrome` on PATH, else Playwright's bundled Chromium. Connect logs which
+ (`[x-session] Opening Chromium 153.0.8010.47 Arch Linux at /usr/bin/chromium (found on PATH)…`).
+- **The bundled build is chromium-1217, Chrome for Testing 147.0.7727.15** (and its headless shell,
+ the same revision): `playwright-core@1.59.1`'s `browsers.json`. `~/.cache/ms-playwright` also holds
+ chromium-1234 (151) and two older revisions; this Playwright does not use them.
+- **The profile/executable pairing.** A connect writes `archilyzer-browser.json` into the profile
+ dir (`{ executablePath | null, version, recordedAt }`). The headless refresh and the Playwright
+ fallback fetcher open the profile with the bundled build (`launchXProfile`) and fall back to the
+ recorded executable only when the bundled launch throws. Measured: a profile written by system
+ Chromium 153 (a cookie added, headed and headless) opens in the bundled headless shell 147 with the
+ cookie's value readable; `Last Version` stays `153.0.8010.47`; 153 re-opens it after. So the
+ fallback is a guard, not the common path.
+- **The X login source, `social.x.cookieSource`** (`common/social/xCookieSource.ts`, pure).
+ `"browser"` | `"profile"`; absent = **resolved at read time, never stored**: `"browser"` when
+ `cookiesFromBrowser` is set and no profile is connected (connected = the broker's exported jar
+ carries an `auth_token`, `readXSessionStatus().looksAuthenticated`), else `"profile"`. A stored
+ choice always wins. The fetch resolves it per channel against the channel's own
+ `cookiesFromBrowser` over the global one (`fetchPosts` → `resolveXCookieSourceFor`).
+ **The source, not `cookieMode`, governs the X fetchers**: the browser source passes the spec
+ whatever the mode; the profile source keeps the old order (the jar, else the `"always"`-mode
+ spec, else a guest run).
+- **gallery-dl 1.32.9** takes `--cookies-from-browser BROWSER[/DOMAIN][+KEYRING][:PROFILE][::CONTAINER]`
+ (yt-dlp's syntax plus `/DOMAIN`) and reads every browser it supports, Chromium's encrypted store
+ included, on each run. The spec is passed verbatim (`galleryDlCookieChoice`).
+- **Reading Firefox's store ourselves** (`common/social/xBrowserLogin.ts`): for the Playwright
+ fallback and the Settings "Check". The store is found as gallery-dl and yt-dlp find it — a
+ profile path or name from the spec, else the most recently modified `cookies.sqlite` under
+ `~/.config/mozilla/firefox`, `~/.mozilla/firefox`, Snap, Flatpak, macOS (two levels deep).
+ `cookies.sqlite` **and its `-wal`** are copied to a private `mkdtemp` dir (a running Firefox
+ keeps fresh rows in the WAL until a checkpoint; a fixture held open in WAL mode proves it), read
+ there, and removed; only X's rows are selected; the profile is never opened in place. A
+ container (`::NAME`) is a `userContextId` from `containers.json` (`::none` = outside every
+ container), as in yt-dlp. `expiry` is seconds (read as ms when too large for seconds);
+ `lastAccessed` is PRTime (µs). The Chromium family is NOT read here (its values are encrypted
+ with a keyring key): the readers say so and gallery-dl reads it.
+- **`node:sqlite`** (Node 22.23 here; unflagged since 22.13, an ExperimentalWarning once per process)
+ is loaded at run time through an assembled specifier with `webpackIgnore`
+ (`common/social/nodeSqlite.ts`), like `playwrightRuntime.ts`: the editor's Turbopack build has
+ nothing to resolve, and the repo's `@types/node` (20) has no types for it, so they are structural.
+- **The e2e never reads a real browser.** `x-session.spec.ts` writes a Firefox store under
+ `test-transcripts/` (`common/social/__fixtures__/firefoxCookieStore.ts`, imported by relative path)
+ and points `cookiesFromBrowser` at it as `firefox:<abs path>` — a profile PATH in the spec, so no
+ home directory is searched.
diff --git a/plans/release-16.md b/plans/release-16.md
@@ -25,7 +25,7 @@ slice's prompt carries its ruling, and this record carries what was built. Rules
| DX | `r16/research-setup` | The research-only setup (source → `pnpm install` → `claude mcp add archilyzer` → `/ask`) in one place, the homepage's AI and MCP doc; the sites' and the hub's Use-with-AI page removed and its links pointed at the doc; `README.md` §1/§4 and `mcp/README.md` their own copies (as amended) | `homepage/content/docs/ai-and-mcp.md`, `export/app/use-with-ai/` (removed), the Use with AI links (`export/app/components/{Header,MobileMenu,Footer}.tsx`, `export/app/(workspace)/ask/page.tsx`), `common/lib/{project,corpus}.ts` + `common/bin/compose-site.ts` (what named the page), `mcp/README.md`, `README.md` §1/§4 (wording only), `homepage/e2e/docs.spec.ts`, `export/e2e{,-hub}/use-with-ai-link.spec.ts` and the specs that visited the page |
| FK | `r16/forms-keep-input` | Every editor form keeps what was typed when its action fails: actions return the submitted values with the error, the shared field helpers seed from them | new `editor/app/lib/formState.ts` + test; `editor/app/components/forms/Field.tsx` and the local `Field`s in `SiteForm.tsx`, `ChannelForm.tsx`; every action that returns `{ok:false,error}`/`{error}` (sites, settings, operations/settingsActions, scheduler, storage, homepageActions, cutReleaseAction, channels, videoActions); the 15 forms the ruling lists; e2e `forms-keep-input.spec.ts` (new) + the existing `sites-crud`, `settings`, `channels` specs; records: `plans/FACTS.md`. As shipped, also `editor/app/components/forms/Controlled.tsx` (new) and one tag swap each in `DurationField`, `SocialLinksField`, `SiteMembershipsSection`, `WorkersField` ("Slice FK, as shipped") |
| RM | `r16/move-holds-writers` | A media move holds the channel's writers and mirrors its copy, so a transcription or download during the move cannot fail it | `common/controller/relocateChannelMedia.ts` (+ the saved-video mover if it shares the code) + tests; the lane runners' per-channel skip (`common/controller/autoRunner.ts`, the backfill/digest/normalize/clip-fetch entry points, `common/lib/channelMediaHold.ts`); `common/jobs/jobKinds.ts` (`needsMedia`); the channel page's Storage panel and the rack's reason text; `editor/e2e/relocate*.spec.ts`; records: `plans/FACTS.md` |
-| XL | `r16/x-login` | Connecting an X account works: the Connect window is the operator's own browser without automation signals, and the fetchers can use the operator's browser login directly (`cookiesFromBrowser`) with no window at all | `common/social/{xSessionBroker,xGalleryDlFetcher,fetchers,playwrightRuntime}.ts` + tests; `common/lib/settingsSchema.ts` (one key) + `SETTINGS.md`; `editor/app/settings/{xSessionActions.ts,components/XSessionSection.tsx}`; `editor/e2e/settings*.spec.ts`; records: `plans/FACTS.md`, `ENVIRONMENT.md` if an env var is added |
+| XL | `r16/x-login` | Connecting an X account works: the Connect window is the operator's own browser without automation signals, and the fetchers can use the operator's browser login directly (`cookiesFromBrowser`) with no window at all | `common/social/{xSessionBroker,xGalleryDlFetcher,fetchers,playwrightRuntime}.ts` + tests; `common/lib/settingsSchema.ts` (one key) + `SETTINGS.md`; `editor/app/settings/{xSessionActions.ts,components/XSessionSection.tsx}`; `editor/e2e/settings*.spec.ts`; records: `plans/FACTS.md`, `ENVIRONMENT.md` if an env var is added. As shipped, also `common/social/{xBrowser,xCookieSource,xBrowserLogin,nodeSqlite}.ts` and `__fixtures__/firefoxCookieStore.ts` (new), `xPlaywrightFetcher.ts`, `common/controller/fetchPosts.ts` (the source per channel), `common/lib/{settingsDocs,envVars}.ts`, `editor/app/settings/page.tsx`, and `editor/e2e/x-session.spec.ts` — the spec that covers the section ("Slice XL, as shipped") |
## Slice CK — the ruling (2026-09-30)
@@ -785,6 +785,169 @@ FK row.
`new-channel-onboarding`, `social-channel`, `sites-crud`, `settings`: **70 passed**, 0 failed,
5.6 min. The full suite was not re-run, as the parent directed.
+### Slice XL, as shipped — connecting an X account works (2026-10-01)
+
+Branch `r16/x-login` off `main` `a2229d68`, worktree `~/Projects/r12-source-mirror` (editor 5101,
+test 5111, export 5110), one Opus implementer. Scratch files `xl-*` in the job's `tmp`; the
+measurements behind the launch options are `xl-exp/` (two probe scripts, their screenshots, and a
+smoke of the slice's own modules). The ruling is above ("Slice XL — the ruling").
+
+**Measured first** — Playwright 1.59.1 driving system Chromium 153 and the bundled build, headed
+under Xvfb (never on the operator's display) and headless, on scratch profiles; no page ever
+loaded x.com or Google:
+
+| Launch | `navigator.webdriver` | The window's bar |
+|---|---|---|
+| Playwright's defaults, system Chromium | true | "Chrome is being controlled by automated test software" |
+| `ignoreDefaultArgs: ["--enable-automation"]`, sandbox on | **true** — the debugging pipe sets it | none |
+| + `--disable-blink-features=AutomationControlled`, Playwright's `--no-sandbox` | false | "You are using an unsupported command-line flag: --no-sandbox" |
+| the same, sandbox on | false | "… unsupported command-line flag: --disable-blink-features=AutomationControlled" |
+| + `--test-type` | false | none |
+| the bundled Chrome for Testing, the same options, sandbox on or off | false | none |
+
+So the blink flag is what turns `navigator.webdriver` off, and `--test-type` plus the sandbox is
+what keeps a system Chrome's window free of a warning bar. **The bundled build is chromium-1217,
+Chrome for Testing 147** (`playwright-core@1.59.1`'s `browsers.json`), not the cache's
+chromium-1234 (151), which this Playwright does not use. **A profile written by system Chromium 153
+opens in the bundled headless shell 147 with its cookies readable** (a cookie added in 153, read back
+by value in 147; `Last Version` stays 153; 153 re-opens it after), so the refresh keeps the bundled
+build and the recorded executable is a fallback, not the common path.
+
+**What it does.**
+- **The Connect window is the operator's own browser without the automation signals**
+ (`common/social/xBrowser.ts`, used by `connectXAccount`): `ARCHILYZER_X_BROWSER` (a path or a name
+ on PATH; one that is not an executable refuses the connect — it never opens another browser),
+ else the first of `chromium`, `google-chrome`, `google-chrome-stable`, `chrome` on PATH, else the
+ bundled Chromium. One pure options builder, `buildXBrowserLaunchOptions`:
+ `ignoreDefaultArgs: ["--enable-automation"]` only (never `true`: that would drop
+ `--password-store=basic` and a system Chromium would encrypt the profile's cookies with the desktop
+ keyring's key, unreadable by the bundled refresh), `--disable-blink-features=AutomationControlled`,
+ `--test-type`, the sandbox on for the headed window (retried without it, and logged, when the host
+ cannot start one). Connect logs the browser (`[x-session] Opening Chromium 153.0.8010.47 Arch
+ Linux at /usr/bin/chromium (found on PATH). …`) and writes it into the profile
+ (`archilyzer-browser.json`); its success note names it.
+- **The profile opens headless through one function**, `launchXProfile` (the refresh and the
+ Playwright fallback fetcher): the bundled build with the same signals dropped, and the recorded
+ executable only when the bundled launch throws.
+- **The login source, `social.x.cookieSource`** (`common/social/xCookieSource.ts`, pure; the one new
+ settings key, `SETTINGS.md` and `settings.json.example` regenerated): `"browser"` | `"profile"`,
+ absent by default and **resolved at read time** — `"browser"` when `cookiesFromBrowser` is set and
+ no profile is connected (no exported jar carrying an `auth_token`), else `"profile"`. `fetchPosts`
+ resolves it per channel (the channel's own `cookiesFromBrowser` over the global) and hands the X
+ fetchers `cookieSource` and `browserCookies` (`PostFetchInput`, `common/social/fetchers.ts`).
+ **The source, not `cookieMode`, governs X**: the browser spec is passed whatever the mode.
+ - **gallery-dl** (`galleryDlCookieChoice`, pure): browser → `--cookies-from-browser <spec>` verbatim
+ (gallery-dl's `BROWSER[/DOMAIN][+KEYRING][:PROFILE][::CONTAINER]`), never the jar, fresh on every
+ run; browser with no spec → a guest run that says why; profile → the jar, else the
+ `"always"`-mode spec, else a guest (the order before the choice existed). The job log names the
+ source each run.
+ - **The Playwright fallback**: browser → a fresh headless context carrying the cookies
+ `xCookiesFromBrowser` reads now; a browser it cannot read refuses by name instead of running
+ logged out. Profile → `launchXProfile`, as before.
+ - **Nitter** uses no X login and is unchanged.
+- **`xCookiesFromBrowser(spec)` and `readXLoginStatus(paths, settings)`**
+ (`common/social/xBrowserLogin.ts`). The read takes the resolved SPEC, not the settings, so a
+ channel's own `cookiesFromBrowser` holds. **Firefox only, read-only**: the store is found as
+ gallery-dl and yt-dlp find it (a profile path or name, else the most recently modified
+ `cookies.sqlite` under the Firefox roots), `cookies.sqlite` and its `-wal` are copied into a private
+ `mkdtemp` dir, X's rows alone are selected, the copy is removed; a `::CONTAINER` is honoured.
+ **Chromium's encrypted store is not read here** — gallery-dl (and yt-dlp) read it; for such a spec
+ the Check and the fallback say so. The status: the source in use and whether it was chosen, whether
+ an `auth_token` for x.com is visible (`null` when the source cannot be read), and when it was last
+ seen — the browser's own `lastAccessed` for that cookie, or the jar's export time for the profile.
+ `node:sqlite` is loaded at run time (`common/social/nodeSqlite.ts`).
+- **The Settings section** (`XSessionSection.tsx`, `xSessionActions.ts`, `settings/page.tsx`): the
+ intro names the two sources; a **Login source** select (Automatic — now: …, Browser login,
+ Connected profile) saved on change through `saveSettings` (Automatic stores `social.x` empty); the
+ source in use; **Check**, whose result is one line; the Connect text says the window is the
+ operator's own browser, that Google's sign-in may still refuse an embedded browser, and that X's
+ password login is the reliable path. Every session action also returns the source in use
+ (connecting or forgetting a profile can move the default).
+- `ARCHILYZER_X_BROWSER` is declared in `common/lib/envVars.ts` (runtime) and `ENVIRONMENT.md`
+ regenerated.
+
+**New accessible names (contracts from here on):** `x cookie source` (the select — `getByLabel`
+needs `{ exact: true }`, the next one contains it), `x cookie source in use`, `check x login`,
+`x login status`. Unchanged: `x session state`, `connect x account`, `refresh x cookies`,
+`clear x session`, the `data-x-session` hook and the `role="status"` / `role="alert"` lines.
+
+**Commits**
+
+| Commit | What |
+|---|---|
+| `cfb6dd10` | `common:` the Connect browser and its options, the login source, the browser-login reader and status, the fetchers, the `social` settings block; SETTINGS.md, settings.json.example, ENVIRONMENT.md |
+| `a266b18a` | `editor:` the X account session's source select, Check and Connect text; the actions; `x-session.spec.ts` |
+| this commit | `plans:` this section; FACTS; the editor changelog |
+
+#### Gates (logs `$T/xl-*.log`)
+
+- **tsc** (all workspaces) clean, 126 s, at `a266b18a`'s tree — after deleting the worktree's stale
+ `export/.next/types` (an old build's, still naming the removed `/use-with-ai` page).
+- **common:** 2,439/2,439, 113 s — the new `xBrowser.test.ts` (9), `xCookieSource.test.ts` (7),
+ `xBrowserLogin.test.ts` (11), `xGalleryDlFetcher.test.ts` (7) and one more `settingsSchema` case;
+ `settingsDocs`, `envVars` (both generated files) and `architecture` green.
+ `archilyzer docs files --check` clean. **Editor unit:** 109/109 (unchanged).
+- **Build:** the capped editor build with the corpus linked (the worktree's own `transcripts/` set
+ aside, `ln -sT` the primary's, `systemd-run --scope -p MemoryMax=5G`, `pnpm --filter editor exec
+ next build`, the link removed and the directory put back): exit 0, 73 s, 1.64 GB peak RSS.
+- **Smoke of the slice's modules** (`xl-exp/smoke.mts`, under Xvfb, a scratch corpus, no
+ navigation): `findXBrowser` picked `/usr/bin/chromium`; the Connect options gave
+ `navigator.webdriver === false`; the record was written; `launchXProfile` opened the profile with
+ the bundled headless build and read the cookie back by value, `navigator.webdriver === false`.
+- **Numbers tool:** none.
+
+ | Run | At | Specs | Result |
+ |---|---|---|---|
+ | 1 | `a266b18a` | `x-session`, `settings`, `cookies-mode`, `forms-keep-input`, `social-channel`, `social-fetcher-picker` | XL_E2E_1 |
+
+**Not tested, and cannot be here:** a real Google or X login in the Connect window, and gallery-dl
+or the fallback against x.com — the suite never loads X, and no test reads a real browser profile.
+The operator verifies by hand after the restart (below).
+
+#### Found and left
+
+- **Google may still refuse.** The debugging pipe Playwright drives the window through is still
+ there, and a sign-in page can look for more than `navigator.webdriver`. The section says so and
+ points at X's password login.
+- **The headless paths still say `HeadlessChrome`** in their user agent (the refresh and the
+ Playwright fallback); only `navigator.webdriver` is off there.
+- **Chromium-family specs are read by gallery-dl only.** With `cookiesFromBrowser` naming chrome or
+ chromium, the browser source works for gallery-dl; the Check reports "cannot be read here" (`null`,
+ not "no login") and the Playwright fallback refuses. Reading Chromium's store would mean the
+ keyring decryption gallery-dl and yt-dlp already carry.
+- **The default reads "connected" as "an exported jar with an auth_token".** A profile that is
+ logged in but has never exported (Connect closed before the jar was written) counts as not
+ connected, so the default stays on the browser until a refresh or a connect writes the jar.
+- **The ExperimentalWarning for `node:sqlite`** prints once per editor process, the first time the
+ Check or the fallback reads a browser store.
+- **Not run against the operator's own Firefox.** The session's tool permissions refused reading
+ the operator's real Firefox profile, so whether `firefox` resolves to the profile the operator
+ logs in to X with is the first thing the Check shows after the restart.
+
+#### Decisions the operator could overturn
+
+| What I did | The alternative |
+|---|---|
+| `--test-type` in the launch args and the sandbox on for the headed window, so a system Chrome draws no warning bar | The ruling's two flags alone: the window shows "unsupported command-line flag" for the blink flag (or for `--no-sandbox`) |
+| The refresh and the fallback drop the same signals headless (`launchXProfile`) | Leave them on Playwright's defaults; the ruling named only the Connect window |
+| `ARCHILYZER_X_BROWSER` is a runtime variable read in `xBrowser.ts` | A `paths` entry in `getPaths()` beside `GALLERY_DL_BIN` (it would add a field every `Paths` literal must carry) |
+| "Automatic" is a third option in the select, storing nothing | Two options only: the read-time default would apply until the first choice and never again |
+| The browser source passes the spec whatever `cookieMode` says | Gate it on `cookieMode` like yt-dlp's cookies (`defer` would then run X as a guest) |
+| The browser source's Playwright fallback refuses a browser it cannot read | Fall back to the profile, or run logged out |
+| `xCookiesFromBrowser(spec)` takes the resolved spec | The prompt's `(paths, settings)`: a channel's own `cookiesFromBrowser` would be ignored |
+
+#### After the restart, by hand (the operator)
+
+1. `/settings` → X account session: **In use** should read `Browser login (firefox) (automatic)`
+ (the live `settings.json` has `cookiesFromBrowser: "firefox"` and no exported jar). **Check**: an
+ X login visible in firefox, with when its `auth_token` was last used. If it says none, log in to
+ x.com in that Firefox profile and Check again.
+2. One X channel's **Fetch posts**: the job log says `X login: the browser (firefox), read by
+ gallery-dl.` before gallery-dl runs.
+3. Optionally **Connect X account**: the window should be Chromium 153, with no "controlled by
+ automated test software" bar; log in with X's username and password (not Google), close the
+ window. The source stays as chosen; on Automatic it moves to the connected profile.
+
## Rollout
Both slices are export- and homepage-side; the editor and umtool are not rebuilt for this release.