Skip to content

fix(desktop): recover a session the engine no longer accepts - #3

Merged
tyza66 merged 7 commits into
mainfrom
codex/fix-desktop-unauthorized-recovery
Oct 23, 2026
Merged

tyza66 merged 7 commits into
mainfrom
codex/fix-desktop-unauthorized-recovery

Conversation

@tyza66

@tyza66 tyza66 commented Oct 23, 2026

Copy link
Copy Markdown
Member

Problem

The desktop window could suddenly strand on the engine's plain text 401 page,
"alpha web authentication required; reopen the URL printed by alpha web."
The health probe counts a 401 as a live engine (ENGINE_UNAUTHORIZED), so
the watchdog never replaces the host, and the plain text body carries no way
back into the application. Only a manual restart recovers.

Approach

  • packages/client/connection/src/browser-auth.ts: writeUnauthorized
    answers a browser document navigation (sec-fetch-dest: document, not
    HEAD) with a minimal HTML page carrying the same copy plus an inline
    script that calls unauthorized(location.href) only when
    globalThis.dshDesktopBoot?.unauthorized exists. Probes, scripts, and
    command-line callers keep the plain text 401. No denial response carries a
    token.
  • apps/desktop/src-tauri/src/lib.rs: the dshDesktopBoot bridge gains
    unauthorized(href), which emits the alpha-unauthorized window event.
    The shell listens for it, reads the engine URL it recorded when the host
    reported readiness (that URL carries the launch token of the host still
    serving), and navigates the main window back to it. The navigation
    re-mints the cookie and the application reloads.
  • .github/workflows/ci.yml: a new windows-rust-check job runs
    cargo check --locked on the desktop shell and joins the PR verdict job
    all-checks-passed. No other CI lane compiled the Rust crate.

Verification

  • pnpm exec vitest run packages/client/connection: 12 files / 117 tests
    pass.
  • Coverage slice: browser-auth.ts at 100% statements / branches /
    functions / lines, satisfying the connection layer's per-file gate.
  • pnpm exec vitest run packages/host/frontend-static: 1 passed.
  • pnpm run doc-sync: 41 passed, 0 failed (translation pairing, doc
    budgets, md-wrap, md-links, doc-graphs, doc-typecheck, site build).
  • pnpm run typecheck:contracts-ready: exit 0; the pre-push hook re-ran it.
  • pnpm exec oxlint packages/client/connection: clean.
  • Lint baseline: on the untouched tree lint:contracts-ready reports the
    same 10 errors in packages/api/session-controller, unrelated to this
    change.
  • No Rust toolchain on this machine; the Rust side is verified by the new
    CI job.

The desktop window could strand on the engine's 401 denial page. The health
probe counts a 401 as a live engine, so no host restart ever followed, and the
plain text body carried no way back into the application.

A browser document navigation now receives a minimal HTML page whose script
reports the denial to the shell through the `dshDesktopBoot` bridge. The shell
keeps the engine URL the host reported readiness with and navigates the window
back to it, which re-mints the cookie from the launch token only the shell
holds. Every other request keeps the plain text denial, and no denial response
carries a token.

No other CI lane compiles the Rust shell, so a new `windows-rust-check` job
does, and it joins the PR verdict.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

The foreign session readers stamp every converted header with the catalog's
current version, but the manifest never listed the package that publishes it.
Node then only resolved the import when some other consumer had already put
a copy where the walk up from this package would find it, so a profile that
mounted session-controller without that accident failed the row outright with
ERR_MODULE_NOT_FOUND and took the whole composition down with it.

The dependency joins the manifest, and the lockfile records the link.
The Codex and Claude converters each walked their own file with the same loop,
accumulating the same header fields and building the same event envelope, so
every fix to one had to be made twice and one of the two already drifted: the
id fallback split the path on `/` only, which on Windows turned a rollout file
name into a whole absolute path.

Both readers now share the record walk, the session accumulator and the event
envelope, with the narrowing helpers they both reached for lifted into
`json-records`. The shared fallback takes the base name and strips the suffix,
and `node:sqlite` moves inside the functions that open the store so a process
that never reads a ZCode session never loads the experimental module.

The new spec pins what each writer contributes to a converted session, what a
writer leaves out, and the abort and fallback paths.
Sibling rows mount concurrently, so the settings section holding the user's
default model can still be installing when a client's first session/new or
session/resume lands. Reading the selection straight away then found nothing,
the agent reached the loop with no model selected, and the loop failed on the
`{{model}}` placeholder it had no value for.

Both entry points now await the settled tree first, the same barrier the
headless runner already applies before it reads a default.
tauri-build asserts every externalBin path exists and copies it beside the
crate output before a single crate compiles, and the only thing that ever
puts a real Node sidecar there is prepare:tauri-runtime, which needs the
whole workspace payload the check deliberately never installs. The lane
therefore failed before it could compile anything.

A check never launches the shell, so a stand-in file satisfies the assertion
and keeps the lane free of pnpm.
The verify-export-jsdoc gate reads every exported name in a package API, so
`asObject` reached the static lane without the `@param` and `@returns` lines
the contract requires. The helper is shared by the foreign-format adapters
this branch introduced, so the omission lands on the branch rather than on
the package it came from.

Describe the narrowing in the same terms the adapters use: the raw field
value arrives as whatever JSON type the writer emitted, and the object comes
back empty when the value is not one.
The sidecar stand-in let the shell check compile, and tauri-build then stopped
on the next assertion: `bundle.resources` maps `staging/runtime` into the
bundle, and only prepare:tauri-runtime ever puts the payload there. That
script needs the whole workspace payload the check deliberately never
installs, so the lane stages the directory instead and stays free of pnpm.

The real prepare step clears entries it does not own before it writes, so a
directory left behind by a check cannot outlive the next real staging run.
@tyza66
tyza66 merged commit 8fffac4 into main Oct 23, 2026
27 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants