Skip to content

feat(plugins): sign builderlab in through the native fi session in fi builds - #1461

Merged
baxen merged 9 commits into
mainfrom
duncan/fi-plugin-migration
Oct 30, 2026
Merged

baxen merged 9 commits into
mainfrom
duncan/fi-plugin-migration

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Builds on #1424, now on main. In a build with the NIP-FI descriptor (BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG), the bundled Builderlab plugin, including hosted communities, signs in through the native FI session. The adapter session stays in OS secure storage, so sign-in survives restart and reload, and the renderer never sees the token. Builds without FI keep the legacy in-memory OAuth login unchanged.

Backend: Builderlab must accept the adapter session as Authorization: Bearer on GET v1/auth/me and the communities, nostr-identity, known-community and Beekeeper routes. The Apps viewer exchange (v1/auth/hosted-site/native-session) and the backup enroll/upload routes (v1/buzz/nsec-backups*) must accept it too; until they do, an FI build's Apps viewer and automatic backup fail closed. An FI build does not offer Builderlab as a Decisions API provider: native decide sends the legacy credential, which an FI build does not have.

Native

  • federated_identity/access.rs forwards a granted route natively with Authorization: Bearer <adapter session> to the identity origin only.
    • The method and path must match exactly. Callers can't set Authorization, Cookie or X-BB-Session-Credential. Redirects aren't followed.
    • Each call is fenced to the caller's enabled artifact, its live registration, and the session revision the call started under. A sign-out or account switch before native reads the token fails the call with "Federated session changed". A request already sent is not cancelled; the Builderlab clients discard its result because the session changed.
    • Plugin change, reload and recover retire registrations, including one still being admitted, and the service registers again once.
  • The plugin manager gives Builderlab a host.federatedIdentity grant of its exact routes in FI builds, and no network origins or files. Plugin import lists the routes.
  • The Apps viewer and the automatic Nsec backup hand native only the public session revision. Native resolves the token at use time and sends it as Bearer, for the hosted-site exchange and for backup enroll and upload. A backup pass rechecks the revision before each later request and stops as signed out once native retires it, without waiting for the renderer.
  • When the FI viewer can't open because the session revision is stale, the exchange answers 401, or the minted viewer has already expired, the error points to Settings → Identity → Organization sign-in. FI builds have no Builderlab sign-in of their own. Legacy errors still point to Builderlab in Settings.
  • The build refuses a nonempty BUZZ_BUILDERLAB_URL alongside FI, and release.yml blanks it when FI is set.
  • A 401 from a granted call still fails only that call. Native then asks the adapter's GET /v1/session once (FederatedIdentity::recheck), for the revision the call ran under. Only a session the adapter refuses there (session_required / session_expired) ends, as reauthentication-required, the same way an assertion refusal ends it. A recheck that sees a newer revision does nothing.

Plugin

  • session.ts adds a BuilderlabSession seam with two implementations:
    • the legacy createOAuthSession
    • createNativeSession over federatedIdentity, which reads the account (subject, email, can_use_remote_agents) from GET v1/auth/me once per session revision and fails closed with a retry
  • Agents, communities and known-communities call session.request() and no longer take the host or a credential.
  • Vite derives VITE_BUZZ_BUILDERLAB_URL from the FI identity origin and API base. Persisted adapter identities, registration replay keys and the account subject keep the same scope in both modes, so nothing migrates.
  • A 401 under FI fails only that call. The native session decides when it ends, and an ended session shows as signed out with "Your Builderlab sign-in ended. Sign in again."
  • In FI builds the Builderlab card has no Sign out of its own. The session is shared with community sign-in, so the card links to Identity → Organization sign-in (through navigation), where that sign-out lives. Reviewing the connected Buzz identity (the mismatch prompt and Unpair identity) stays in Builderlab's communities view; moving it to Identity is a follow-up.
  • ensureSignedIn settles when the native sign-in attempt finishes, so a sign-in that leaves the snapshot unchanged (unavailable before restore, or unconfigured) can't leave a caller waiting.
  • A Settings view closing during a pending login no longer cancels the shared native sign-in (abandon()).
  • Restoring a saved sign-in and reading its account show as checking, with a busy button and no browser instructions or Cancel; only a browser sign-in can be cancelled. After a successful retry, focus moves to the Identity link only if the retry button still had it.

Review threads addressed

  • Bind requests to the initiating revision: the service captures the session revision, and native sends the token only while that revision is current.
  • Migrate persisted agent scopes: not needed. oauthTarget() and the auth/me subject are the same in both modes, so the stored tuple doesn't change.
  • Durable subject for email-less accounts: the subject comes from GET v1/auth/me, not the FI revision or email.
  • Observer unmount cancelling a shared login: Login calls abandon(), which is a no-op natively.
  • Recover registrations after refused lifecycle operations: a request that hits "Plugin registration retired" drops the cached registration, registers again and retries once. Concurrent requests sharing that registration each retry. The cache is now keyed by the plugin fiber, because this.ctx is a new object on every access and the old cache never hit.

Compose-ctrl

The Builderlab Apps agent tools are made native in the stacked draft #1958.

Validation

At f4eaf7991:

  • biome check, tsc --noEmit and the design typecheck are clean.
  • Full vitest: 14,110 tests passed. New tests cover:
    • the native session
    • an FI-build plugin journey (native sign-in, no OAuth, federated backup handoff, no decisions provider)
    • the session fence
    • retry on retirement, including concurrent requests and retirement during admission
    • FI Login linking to Identity instead of signing out, and focus moving there only from a focused retry
    • restore and account reads shown as checking, with no cancel
    • the ended-session message
  • Default build: cargo test -p buzzodz-plugins --lib passed 44 and cargo test --lib in src-tauri passed 738 (2 ignored), including a native sign-out mid-backup that sends no later request.
  • FI build, with a descriptor whose identity origin is https://app.builderlabstaging.xyz: the same suites passed 44 and 738 (2 ignored).
  • cargo clippy -p Buzz -p buzzodz-plugins --all-targets -D warnings is clean in the default build.
  • Not yet exercised against a live adapter and backend.

Testing an FI build

Unset BUZZ_BUILDERLAB_URL. The FI descriptor supplies the Builderlab authority, and the build fails when both are set ("BUZZ_BUILDERLAB_URL must be removed when FI is configured").

FI build rollout

The release logs checked on October 30 (runs 41106665291 and 41121945380) have an empty BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG. Those builds keep legacy OAuth. Merging this PR does not enable FI.

Before enabling the descriptor, deploy the Bearer support for account and community routes, Apps viewer exchange, and backup enroll/upload. Build a candidate containing this PR with the descriptor set and BUZZ_BUILDERLAB_URL empty. Verify Organization sign-in appears, sign in, restart and confirm the saved session restores, then check account access, communities, Apps viewer and backup. Check sign-out/account changes and refused requests too. These live checks have not been run.

Rollback requires a new build with BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG unset and the legacy Builderlab URL restored. Promote that build before removing server Bearer support. Existing installed binaries retain their compiled configuration; there is no runtime toggle.

CI now builds the frontend and runs the native plugin and host-request grant tests with a public fixture descriptor, after the default native tests.

Generated with Codex

wpfleger96 added a commit that referenced this pull request Oct 21, 2026
The fixture still imported the plugin Login and session modules after they moved to #1461, so the documented manual UI check no longer loaded. Storage failure now keeps the sign-in prompt so its recovery message is visible.

Co-authored-by: Will Pfleger <[email protected]>
Signed-off-by: Will Pfleger <[email protected]>
@wpfleger96
wpfleger96 force-pushed the hayt/native-federated-sessions branch 4 times, most recently from 5aa02fa to 5b8e3da Compare October 28, 2026 18:46
@bradseiler
bradseiler force-pushed the hayt/native-federated-sessions branch from 784b650 to dac3daa Compare October 29, 2026 17:42

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Source-only review found four P2 account/persistence/login defects and one P3 registration-recovery defect. No runtime validation was performed.

Comment thread src/bundled/builderlab/session.ts Resolved
Comment thread src/bundled/builderlab/session.ts Resolved
@bradseiler
bradseiler force-pushed the hayt/native-federated-sessions branch 2 times, most recently from e4d2228 to c228932 Compare October 30, 2026 01:19
@kalvinnchau
kalvinnchau force-pushed the duncan/fi-plugin-migration branch from eedf236 to 7f13560 Compare October 30, 2026 03:18
@kalvinnchau kalvinnchau changed the title feat(plugins): sign Builderlab and Hosted Communities in through native FI feat(plugins): sign builderlab in through the native fi session in fi builds Oct 30, 2026
@kalvinnchau
kalvinnchau changed the base branch from hayt/native-federated-sessions to main October 30, 2026 03:19
@kalvinnchau
kalvinnchau force-pushed the duncan/fi-plugin-migration branch from 53c4034 to 8f47505 Compare October 30, 2026 05:01
@kalvinnchau
kalvinnchau marked this pull request as ready for review October 30, 2026 15:00
@kalvinnchau
kalvinnchau requested review from a team as code owners October 30, 2026 15:00

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Two P3 findings from source review; runtime validation was not performed.

Comment thread crates/plugin-manager/src/lib.rs Resolved

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot coverage.

Reviewed head 8f4750547522ebe94bc086c76a38df6a3e6d0072 against base 8fdb02df62398c0fa4c8769507444f22df86a2c7 (merge-base 110a59145f826a6e157c12d76ec91b97f7a39d61). Findings are non-blocking review comments; this is not an approval.

Product findings are inline. Separately, [P2] Remove internal coordination details from the public PR description. The Backend paragraph includes three links to a private repository's implementation PRs. The destination repository is confirmed private. Replace these with a self-contained description of the required backend capabilities; keep private implementation tracking in the internal discussion. This is a public-material finding, not evidence of a leaked credential. No description images were attached.

Validation: source analysis only; no PR code, tests, builds or app workflows were executed. A single hosted-check snapshot showed CI required successful; Windows validation and browser flake checks were skipped. Live FI/backend interoperability, OS secure storage, account-switch timing, rendered keyboard/screen-reader behavior and performance remain unverified. The PR explicitly identifies backend prerequisites for the Apps viewer and automatic backup; their availability was not established by this review.

Comment thread src-tauri/src/nsec_backup.rs Resolved
Comment thread src/bundled/builderlab/session.ts Resolved
Comment thread src/bundled/builderlab/login/Login.tsx Resolved
kalvinnchau and others added 4 commits October 30, 2026 12:31
… builds

an fi build (BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG set) now signs the
builderlab plugin in through the native nip-fi session instead of its
in-memory oauth credential. the token stays in os secure storage and
the session restores after restart; builds without fi keep the legacy
oauth path unchanged.

- plugin-manager grants builderlab a host.federatedIdentity grant of exact
  routes and no network origins or files in fi builds; BUZZ_BUILDERLAB_URL
  must be empty alongside fi
- native federated_identity_request forwards granted routes with the
  session as a bearer token, fenced to the caller's live registration and
  the session revision the call started under; plugin change, reload and
  recover retire registrations and the service registers again once
- registrations are cached by the calling plugin's fiber: each this.ctx
  access is a new object, so the previous context key never hit
- builderlab consumers take a BuilderlabSession: the legacy oauth session or
  a native session that reads the account from v1/auth/me once per revision
- the apps viewer and nsec backup hand native the public session revision;
  native resolves the token and exchanges or uploads with bearer
- vite derives VITE_BUZZ_BUILDERLAB_URL from the fi descriptor so persisted
  adapter identities and registration keys keep their scope
- apps agent tools stay on the legacy login and are absent in fi builds

Signed-off-by: Kalvin Chau <[email protected]>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <[email protected]>
Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- a granted fi request that answers 401 asks the adapter's `get
  /v1/session` once; only a refused session ends, as
  reauthentication-required. the caller still gets its own 401
- builderlab's native session shows that end as signed out with a sign-in
  prompt
- fi builds drop builderlab's sign out: the login links to identity →
  organization sign-in, which ends the shared session

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
fi builds have no builderlab sign-in of their own, so a stale fi revision
or a 401 from the viewer exchange now names settings -> identity ->
organization sign-in. legacy credential errors keep the builderlab
settings destination.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
- ensuresignedin settles when the native sign-in attempt finishes, so an
  unchanged unavailable or unconfigured snapshot no longer leaves callers
  waiting
- an expired fi viewer cookie points to identity -> organization sign-in
- the bundled builderlab grant test asserts the federated grant and no
  file or network access in fi builds
- the backup handoff test separates a refused hosted fi handoff from the
  foreign-source no-op, and pins that a backup root other than the fi
  identity root never stores a federated session

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
@kalvinnchau
kalvinnchau force-pushed the duncan/fi-plugin-migration branch from 8f47505 to 63bc021 Compare October 30, 2026 19:47
kalvinnchau and others added 2 commits October 30, 2026 13:00
- a registration native retires while federated_identity_register is pending now registers again once, like one retired after admission
- a disposed fiber still rejects without a retry

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
- a backup pass stops as signed out before the mapping read and the upload once native retires its fi revision, without waiting for the renderer handoff
- the fixture can hold any route, to sign out mid-pass

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
…ellable

- the native session marks restore and account reads as checking; login and creation show a busy check instead of browser instructions and cancel
- a successful fi retry moves focus to the identity link only when the retry button still had it

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
@kalvinnchau

Copy link
Copy Markdown
Contributor

🤖 Addressed the public-material finding: the description no longer links private backend PRs. It now states the backend capabilities this PR requires, notes that FI builds don't offer Builderlab decisions, and its Validation section is updated for f4eaf79.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage.

Reviewed head f4eaf7991b15ea3b87bb48951d8a7ddfb2bfd0bb against base 89e2d157ed99b8797da65fca71c1785d40079b6b, comparing the previous review at 8f4750547522ebe94bc086c76a38df6a3e6d0072 through endpoint and range diffs.

No new actionable findings. The earlier backup revision fence, checking-versus-browser-sign-in state, and successful retry focus findings are addressed in source. The public description also removes the private backend coordination links. Registration-admission retirement now participates in the bounded retry while retaining the initiating session revision; the FI Decisions integration remains disabled as documented.

Validation: source analysis only. No tests, builds, app workflows or live adapter/backend operations were executed, and hosted CI was not assessed in this pass. The author's reported test results are not independent verification by this review. Native storage and backend interoperability, rendered keyboard/screen-reader behavior and performance remain unverified; the separately discussed FI-descriptor CI coverage follow-up remains outside this change. Cordis behavior when registration admission rejects after fiber disposal was not independently established; no concrete additional defect was demonstrated. No images are attached to the current description. This COMMENT is not an approval or merge authorization.

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 One lifecycle regression remains in registration retry; details inline. Native storage and live backend interoperability remain unverified.

- the registration disposal effect is installed before native admission, so a retry from a disposed fiber throws before any second register call and no registration is left without cleanup

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source follow-up via Wes’s account, with independent Rocket, Mantis and Groot coverage.

Reviewed head 52fd0acf2b7909b675f3537677ac4f8d0ba4794f against base 89e2d157ed99b8797da65fca71c1785d40079b6b, focusing on the two-file delta since f4eaf7991b15ea3b87bb48951d8a7ddfb2bfd0bb.

No new actionable findings. Installing the cleanup effect before native registration addresses the disposal/admission retry defect reported in comment 4428194332: disposed or unloading fibers fail before a second registration is allocated. The new deferred-admission test covers this ordering, and live-fiber retry remains bounded to one attempt under the initiating session revision. This is the dependency boundary our preceding review explicitly left unverified; the current source trace includes Cordis’s inactive-effect guard and native registration removal.

Validation: source analysis only; no tests, builds, app workflows or live adapter/backend operations were executed, and CI was not assessed. The inspected local Cordis source reports version 4.0.2, matching the lockfile, but its package integrity was not independently verified. Native IPC timing, storage/backend interoperability, rendered focus/accessibility and performance remain unverified. The public description and commit text were inspected; no description images are attached. This is a follow-up review, not a fresh whole-feature certification, approval or merge authorization.

} catch (error) {
// Native retires registrations around every plugin lifecycle change,
// even a refused one or one still being admitted. Register again
// once; admission rechecks the artifact and grant. A disposed fiber

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 [P3] Align the retry comment with the disposal guard

This comment says a disposed fiber cannot be retried, but the new rejection-after-unload test exercises exactly that path: native admission rejects with the string Plugin registration retired, the catch retries, and registration() then stops admission when ctx.effect throws on the inactive context. The ordering comment below describes the actual safeguard. Update this comment to distinguish late fulfillment (Error, no retry) from late retirement rejection (retry stopped by the effect guard), so future changes preserve the lifecycle invariant that prevents the second registration.

Preserve the shared FI session in the new account recovery UI and test the FI build path in native CI.

Co-authored-by: Sarnow <1894dcbafaa4df1ac607a9bf879ac0b7d77fbccb0e0f4619ecc252c8adbad8a2@buzz.block.builderlab.xyz>
Signed-off-by: Brad Seiler <[email protected]>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Star Lord’s automated source follow-up via Wes’s account, integrating independent Rocket, Mantis and Groot coverage.

Reviewed head 28a7ba42300fe9d15939a94dc14de28979b71d35 against base b5747efe32a7fc6df97714f8dd70dfe9a9fff907, following the earlier review at 52fd0acf2b7909b675f3537677ac4f8d0ba4794f.

No new actionable findings in the merge integration. The eight preceding feature patches are unchanged; the merge connects the upstream account-recovery sheet to Identity without ending the shared FI session, preserves the non-cancellable checking state, and adds FI-descriptor build/grant coverage. Reviewed the affected callers, session boundaries, source tests and host-owned focus recovery. The new CI assertions cover selected grants; they do not prove exhaustive client/route equivalence.

Validation is source-only: no tests, builds, app or live adapter/backend workflows were executed by this review. One snapshot of CI run 41137944771 (bound to this head; PR CI uses a synthetic merge tree) showed the new FI/native step still running. Final FI counts, summed test time, slowest-test evidence and completed duration were unavailable, so CI success and cost impact remain unverified. Native storage/backend interoperability, rendered keyboard/accessibility behavior and performance remain unverified. The public description and commit text were inspected; no description images are attached. This is a merge follow-up, not a fresh whole-feature certification, approval or merge authorization.

@baxen baxen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Approving for merge at 28a7ba4 on baxen's request. Packaged release builds are not FI builds: BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG is empty in the current release run, so release.yml keeps the legacy BUZZ_BUILDERLAB_URL and the bundled plugin keeps its OAuth login. The merge-commit conflict resolution in SignInFooter/ModelSettings keeps the legacy branch identical when nativeBuilderlab() is false. CI is green at this head. Before the FI descriptor is set on any release, cash-server #139434 must be deployed.

@baxen
baxen merged commit 8fd0d34 into main Oct 30, 2026
40 checks passed
@baxen
baxen deleted the duncan/fi-plugin-migration branch October 30, 2026 23:15
shellz-n-stuff added a commit that referenced this pull request Oct 31, 2026
Picks up the month-boundary StatusEditor test fix (#2064).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main: (56 commits)
  perf(sidebar): subscribe to selected live and list facts (#1966)
  feat(desktop): align the sign-in callback page with web branding (#2038)
  Show linked PRs in Solo headers and move idle huddles to Chat actions (#1968)
  feat(messages): mark edited messages with "(edited)" (#2020)
  feat(desktop): bundle the buzz CLI in every package (#2027)
  feat(plugins): open a plugin page from buzz://plugin links (#2053)
  feat(plugins): managed plugin sources (#2052)
  test(browser): prevent sampled clock pause races (#1955)
  test: fix month-boundary and Sonner mount races in StatusEditor and RelayStaff tests (#2064)
  test: stop tests leaving temp dirs and a flake-check fixture behind (#1977)
  fix(messages): release Virtua's held range when scrolling stops (#2032)
  test(mcp): allow protection checks in bundled help fixture (#2061)
  fix(emoji): recover transient image failures without reopening chats (#2000)
  test: wait for dialog focus, reach a reading position, re-sign pair() (#1975)
  feat(plugins): sign builderlab in through the native fi session in fi builds (#1461)
  feat(agents): offer bundled Buzz help (#2033)
  fix(github): show pull request status for anchors and subpages (#2055)
  fix(dialogs): focus safe controls and task inputs (#2021)
  fix(composer): truncate workspace names at narrow widths (#2043)
  AIDA-2287: Refresh public channel search incrementally (#2023)
  ...

Signed-off-by: Alex Rosenzweig <[email protected]>
matt2e added a commit that referenced this pull request Nov 2, 2026
The owner's decision: block/buzz-app supports one Builderlab per build
for now, the public deployment (`app.builderlab.xyz`, hosting
`communities.buzz.xyz`). This supersedes `d2e2b7ee9`'s multi-deployment
routing.

- `builderlab-hosted.ts`: keeps only the `communities.buzz.xyz` row
  (host, relay `self` key, portal) and drops the `block.builderlab.xyz`
  and `test.blockstaging.build` rows. `builderlabHosting(found, build)`
  (`{ portal, here } | undefined`) becomes
  `hostedByThisBuilderlab(found, build): boolean`. It is true only when
  the listed deployment hosts the relay (hostname exact or label-boundary
  suffix on the default port, else its `self` key) and its portal is the
  origin of `VITE_BUZZ_BUILDERLAB_URL`. An empty, invalid or other-origin
  build URL, as in an unconfigured or federated-identity build, answers
  false, so such builds never sign in at once. The table stays, so a
  second deployment could be added later.
- `Welcome.tsx`: `onFound` signs in only when `hostedByThisBuilderlab`
  holds and otherwise pauses on the connect choice. The `elsewhere` state
  and prop are gone. `finishJoin` saves an admitted join to the account
  as it did before `d2e2b7ee9`, without the other-Builderlab guard.
- `ConnectChoice.tsx`: back to its single layout. "Buzz relies on a key
  for your identity.", prominent Sign in with email, small I already
  have a key, and Back. The key-only layout and its "different
  Builderlab" copy are gone.

So a community another Builderlab deployment runs, such as a
`block.builderlab.xyz` relay typed into a public build, now gets the
ordinary Connect to {host} choice.

Tests: `builderlab-hosted.test.ts` is rewritten for the boolean. It
covers the `communities.buzz.xyz` apex, subdomain, case, final-dot and
`:443` forms, and every lookalike and non-default-port refusal (now
including a bb-block host). It also covers build URLs of "", "not a
url", the API-path form and `https://block.builderlab.xyz`, and the
`self` fallback with bb-public's key, on the default port only. A new
assertion makes sure an unknown relay with no build URL never matches.
`identity.test.tsx`: the hosted journey runs once, with a public build
URL stubbed. The two key-only "another Builderlab" journeys and the
not-saved join journey are removed. A new 2-case journey covers a bb-block
URL in a public build and a bb-public URL in a build with no Builderlab
URL: each pauses on the ordinary choice (Sign in with email, I already
have a key, Back) with no sign-in started.

Docs: `docs/identity.md` describes one Builderlab per build and the
ordinary choice for every other community. It adds a note that
federated-identity builds do not yet support the first-run flow: Sign in
with email needs the bundled Builderlab plugin's own network grant,
which an FI build does not have (#1461), so sign-in fails after the
browser step. `docs/configuration.md` has the same note under the FI
build input. `docs/buzz-help.md` drops the "different Builderlab"
sentence.

Validation: `vitest run` of `builderlab-hosted.test.ts`,
`identity.test.tsx`, `first-run-join-handoff.test.tsx` and
`first-run-navigation.test.tsx` (4 files, 158 tests) pass.
`tsc --noEmit` and Biome on the changed source files pass. Not run: the
full suite, hooks, lint, `design:check` and native builds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 2, 2026
The owner's decision: block/buzz-app supports one Builderlab per build
for now, the public deployment (`app.builderlab.xyz`, hosting
`communities.buzz.xyz`). This supersedes `d2e2b7ee9`'s multi-deployment
routing.

- `builderlab-hosted.ts`: keeps only the `communities.buzz.xyz` row
  (host, relay `self` key, portal) and drops the `block.builderlab.xyz`
  and `test.blockstaging.build` rows. `builderlabHosting(found, build)`
  (`{ portal, here } | undefined`) becomes
  `hostedByThisBuilderlab(found, build): boolean`. It is true only when
  the listed deployment hosts the relay (hostname exact or label-boundary
  suffix on the default port, else its `self` key) and its portal is the
  origin of `VITE_BUZZ_BUILDERLAB_URL`. An empty, invalid or other-origin
  build URL, as in an unconfigured or federated-identity build, answers
  false, so such builds never sign in at once. The table stays, so a
  second deployment could be added later.
- `Welcome.tsx`: `onFound` signs in only when `hostedByThisBuilderlab`
  holds and otherwise pauses on the connect choice. The `elsewhere` state
  and prop are gone. `finishJoin` saves an admitted join to the account
  as it did before `d2e2b7ee9`, without the other-Builderlab guard.
- `ConnectChoice.tsx`: back to its single layout. "Buzz relies on a key
  for your identity.", prominent Sign in with email, small I already
  have a key, and Back. The key-only layout and its "different
  Builderlab" copy are gone.

So a community another Builderlab deployment runs, such as a
`block.builderlab.xyz` relay typed into a public build, now gets the
ordinary Connect to {host} choice.

Tests: `builderlab-hosted.test.ts` is rewritten for the boolean. It
covers the `communities.buzz.xyz` apex, subdomain, case, final-dot and
`:443` forms, and every lookalike and non-default-port refusal (now
including a bb-block host). It also covers build URLs of "", "not a
url", the API-path form and `https://block.builderlab.xyz`, and the
`self` fallback with bb-public's key, on the default port only. A new
assertion makes sure an unknown relay with no build URL never matches.
`identity.test.tsx`: the hosted journey runs once, with a public build
URL stubbed. The two key-only "another Builderlab" journeys and the
not-saved join journey are removed. A new 2-case journey covers a bb-block
URL in a public build and a bb-public URL in a build with no Builderlab
URL: each pauses on the ordinary choice (Sign in with email, I already
have a key, Back) with no sign-in started.

Docs: `docs/identity.md` describes one Builderlab per build and the
ordinary choice for every other community. It adds a note that
federated-identity builds do not yet support the first-run flow: Sign in
with email needs the bundled Builderlab plugin's own network grant,
which an FI build does not have (#1461), so sign-in fails after the
browser step. `docs/configuration.md` has the same note under the FI
build input. `docs/buzz-help.md` drops the "different Builderlab"
sentence.

Validation: `vitest run` of `builderlab-hosted.test.ts`,
`identity.test.tsx`, `first-run-join-handoff.test.tsx` and
`first-run-navigation.test.tsx` (4 files, 158 tests) pass.
`tsc --noEmit` and Biome on the changed source files pass. Not run: the
full suite, hooks, lint, `design:check` and native builds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 2, 2026
The owner's decision: block/buzz-app supports one Builderlab per build
for now, the public deployment (`app.builderlab.xyz`, hosting
`communities.buzz.xyz`). This supersedes `d2e2b7ee9`'s multi-deployment
routing.

- `builderlab-hosted.ts`: keeps only the `communities.buzz.xyz` row
  (host, relay `self` key, portal) and drops the `block.builderlab.xyz`
  and `test.blockstaging.build` rows. `builderlabHosting(found, build)`
  (`{ portal, here } | undefined`) becomes
  `hostedByThisBuilderlab(found, build): boolean`. It is true only when
  the listed deployment hosts the relay (hostname exact or label-boundary
  suffix on the default port, else its `self` key) and its portal is the
  origin of `VITE_BUZZ_BUILDERLAB_URL`. An empty, invalid or other-origin
  build URL, as in an unconfigured or federated-identity build, answers
  false, so such builds never sign in at once. The table stays, so a
  second deployment could be added later.
- `Welcome.tsx`: `onFound` signs in only when `hostedByThisBuilderlab`
  holds and otherwise pauses on the connect choice. The `elsewhere` state
  and prop are gone. `finishJoin` saves an admitted join to the account
  as it did before `d2e2b7ee9`, without the other-Builderlab guard.
- `ConnectChoice.tsx`: back to its single layout. "Buzz relies on a key
  for your identity.", prominent Sign in with email, small I already
  have a key, and Back. The key-only layout and its "different
  Builderlab" copy are gone.

So a community another Builderlab deployment runs, such as a
`block.builderlab.xyz` relay typed into a public build, now gets the
ordinary Connect to {host} choice.

Tests: `builderlab-hosted.test.ts` is rewritten for the boolean. It
covers the `communities.buzz.xyz` apex, subdomain, case, final-dot and
`:443` forms, and every lookalike and non-default-port refusal (now
including a bb-block host). It also covers build URLs of "", "not a
url", the API-path form and `https://block.builderlab.xyz`, and the
`self` fallback with bb-public's key, on the default port only. A new
assertion makes sure an unknown relay with no build URL never matches.
`identity.test.tsx`: the hosted journey runs once, with a public build
URL stubbed. The two key-only "another Builderlab" journeys and the
not-saved join journey are removed. A new 2-case journey covers a bb-block
URL in a public build and a bb-public URL in a build with no Builderlab
URL: each pauses on the ordinary choice (Sign in with email, I already
have a key, Back) with no sign-in started.

Docs: `docs/identity.md` describes one Builderlab per build and the
ordinary choice for every other community. It adds a note that
federated-identity builds do not yet support the first-run flow: Sign in
with email needs the bundled Builderlab plugin's own network grant,
which an FI build does not have (#1461), so sign-in fails after the
browser step. `docs/configuration.md` has the same note under the FI
build input. `docs/buzz-help.md` drops the "different Builderlab"
sentence.

Validation: `vitest run` of `builderlab-hosted.test.ts`,
`identity.test.tsx`, `first-run-join-handoff.test.tsx` and
`first-run-navigation.test.tsx` (4 files, 158 tests) pass.
`tsc --noEmit` and Biome on the changed source files pass. Not run: the
full suite, hooks, lint, `design:check` and native builds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
wesbillman pushed a commit that referenced this pull request Nov 3, 2026
…ion (FOUNDATION: App.tsx, services.ts) (#2249)

## Summary

New users of FI builds (the internal Block build, 0.7.7) couldn't finish
first run. **Sign in with email** opened the browser, and then the app
showed *Couldn't sign in. Could not complete Builderlab sign-in.*
Welcome exchanged the code through the bundled Builderlab plugin's
network grant. FI builds don't have that grant: Builderlab calls go
through the native NIP-FI session instead
([#1461](#1461)).
[#1820](#1820) documented this as
a known gap.

With this change, Welcome in FI builds signs in through the native
NIP-FI session and sends setup's account calls through its forwarder.
Non-FI builds are unchanged.

- **Sign-in:** `firstRunCredential` calls `federatedIdentity.signIn()`.
The browser step, code exchange and token all stay native. It then reads
`GET v1/auth/me` through the forwarder. The returned credential's value
is the session's public revision. If a session already exists, it signs
in again so the browser can choose the account. A renewal that doesn't
finish (canceled or timed out in the browser) gets back the old session
at the same revision, so first run refuses rather than continuing as the
old account. Native's bare failure codes map to the same copy as the
community sign-in.
- **Account calls:** identity check, link and unlink, owned communities,
and known communities use the same routes through the forwarder via
`federatedBuilderlabAccount`. Each call is checked against the session
revision that sign-in returned, so a session that changed refuses
without sending anything.
- **Acting as the bundled installation:** native FI admission checks
only the grant for the (id, revision) pair, not whether the plugin is
running. So Welcome calls as `block.builderlab`/`bundled` through
`ctx.extend({ pluginOwner })`, the same installation `bundledHost`
already acts as. No native or grant changes.
- **Backup:** `backUpWith` hands the uploader the FI session revision
(`handoffFederatedBackupSession`). Backup status returns "unavailable".
The forwarder has no route for it, and the FI plugin doesn't read one
either, so Welcome back offers no restore and an imported key is offered
a backup. The Block build has no backup configured today.
- **After Welcome:** the native session persists, so `LaunchSignIn`
finds it already signed in and doesn't prompt again.

**Known gap:** after a first-run backup, `backUpWith` clears the
uploader's `builderlab` slot as it does today. The plugin shares the FI
session, so it won't hand that slot off again until the session changes.
This only matters where backup is configured, and the Block build
doesn't configure it.

**FOUNDATION:** `services.ts` gets one line to create the bundled-owner
FI view, only in FI builds. `App.tsx` gets one prop to pass that view to
Welcome.

Out of scope: `browser.ts` still hides native error strings. This is
deliberate, so provider bodies are never forwarded. The "Sign in with
email" label also stays as is in FI builds.

## Validation

- Full `vitest run`: 873 files and 15,427 tests passed (7 files and 21
tests skipped). `tsc --noEmit` and `biome check` are clean. These ran on
the tree committed as `41388fb64`.
- New tests:
- `first-run.test.ts`: the bundled-owner view registers and requests as
`block.builderlab`/`bundled` with a real cordis `Context`. The FI
sign-in reads the account and never opens the plugin's browser handoff.
Failures map to the right copy, including a renewal that doesn't finish,
a native failure code, and an account read that isn't JSON. Cancel calls
`cancelSignIn`.
- `builderlab-account.test.ts`: forwarder paths and headers, revision
fencing, backup status "unavailable", and the federated backup handoff.
- `identity.test.tsx`: Welcome's defaults route through the FI session.
- I mutation-checked the owner and Welcome-wiring tests: each fails when
the change is reverted.
- **Real FI build, step 2 only:** I ran a debug desktop build with the
production bb-block descriptor, on a fresh profile with its own HOME and
keychain. Sign-in now reaches **Welcome back** (the test account already
has a linked key). On `main`, the same binary shows *Couldn't sign in*.
I didn't run steps 3–4 below (Create my key, Cancel sign-in) in a real
FI build.

## Before and after

Both runs use the same FI debug build (bb-block descriptor) on a fresh
profile. Only the frontend differs. I cut the browser sign-in step, and
blacked out the account's email and npub.

### Before: `main` (`90ce5e37a`) shows *Couldn't sign in*


https://github.com/user-attachments/assets/3c04ff79-f020-4123-b226-09aeafa15c24

### After: this PR (`373d7484d`) lands on Welcome back


https://github.com/user-attachments/assets/b109f807-7a4a-4dd3-8fef-f199ec76523c

## Human test (FI build, bb-block staging)

1. Build and run with the staging descriptor from
`squareup/buzz-releases` `scripts/build-macos-app-staging.sh`, and use a
fresh profile with no key:
`BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG='<staging descriptor>'
BUZZ_BUILDERLAB_URL= just desktop`
2. Click **Sign in with email** and finish the browser step. You should
land on **Create my key** (or **Welcome back** for an account that
already has a linked key), not *Couldn't sign in*.
3. Click **Create my key**. Setup should link the key and continue. The
app should open without a second organization sign-in prompt.
4. Run it again on a fresh profile and on one already signed in to the
organization. On each, click **Cancel sign-in** during the browser step.
You should return to the landing page with no key created.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <[email protected]>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
…ion (FOUNDATION: App.tsx, services.ts) (block#2249)

## Summary

New users of FI builds (the internal Block build, 0.7.7) couldn't finish
first run. **Sign in with email** opened the browser, and then the app
showed *Couldn't sign in. Could not complete Builderlab sign-in.*
Welcome exchanged the code through the bundled Builderlab plugin's
network grant. FI builds don't have that grant: Builderlab calls go
through the native NIP-FI session instead
([block#1461](block#1461)).
[block#1820](block#1820) documented this as
a known gap.

With this change, Welcome in FI builds signs in through the native
NIP-FI session and sends setup's account calls through its forwarder.
Non-FI builds are unchanged.

- **Sign-in:** `firstRunCredential` calls `federatedIdentity.signIn()`.
The browser step, code exchange and token all stay native. It then reads
`GET v1/auth/me` through the forwarder. The returned credential's value
is the session's public revision. If a session already exists, it signs
in again so the browser can choose the account. A renewal that doesn't
finish (canceled or timed out in the browser) gets back the old session
at the same revision, so first run refuses rather than continuing as the
old account. Native's bare failure codes map to the same copy as the
community sign-in.
- **Account calls:** identity check, link and unlink, owned communities,
and known communities use the same routes through the forwarder via
`federatedBuilderlabAccount`. Each call is checked against the session
revision that sign-in returned, so a session that changed refuses
without sending anything.
- **Acting as the bundled installation:** native FI admission checks
only the grant for the (id, revision) pair, not whether the plugin is
running. So Welcome calls as `block.builderlab`/`bundled` through
`ctx.extend({ pluginOwner })`, the same installation `bundledHost`
already acts as. No native or grant changes.
- **Backup:** `backUpWith` hands the uploader the FI session revision
(`handoffFederatedBackupSession`). Backup status returns "unavailable".
The forwarder has no route for it, and the FI plugin doesn't read one
either, so Welcome back offers no restore and an imported key is offered
a backup. The Block build has no backup configured today.
- **After Welcome:** the native session persists, so `LaunchSignIn`
finds it already signed in and doesn't prompt again.

**Known gap:** after a first-run backup, `backUpWith` clears the
uploader's `builderlab` slot as it does today. The plugin shares the FI
session, so it won't hand that slot off again until the session changes.
This only matters where backup is configured, and the Block build
doesn't configure it.

**FOUNDATION:** `services.ts` gets one line to create the bundled-owner
FI view, only in FI builds. `App.tsx` gets one prop to pass that view to
Welcome.

Out of scope: `browser.ts` still hides native error strings. This is
deliberate, so provider bodies are never forwarded. The "Sign in with
email" label also stays as is in FI builds.

## Validation

- Full `vitest run`: 873 files and 15,427 tests passed (7 files and 21
tests skipped). `tsc --noEmit` and `biome check` are clean. These ran on
the tree committed as `41388fb64`.
- New tests:
- `first-run.test.ts`: the bundled-owner view registers and requests as
`block.builderlab`/`bundled` with a real cordis `Context`. The FI
sign-in reads the account and never opens the plugin's browser handoff.
Failures map to the right copy, including a renewal that doesn't finish,
a native failure code, and an account read that isn't JSON. Cancel calls
`cancelSignIn`.
- `builderlab-account.test.ts`: forwarder paths and headers, revision
fencing, backup status "unavailable", and the federated backup handoff.
- `identity.test.tsx`: Welcome's defaults route through the FI session.
- I mutation-checked the owner and Welcome-wiring tests: each fails when
the change is reverted.
- **Real FI build, step 2 only:** I ran a debug desktop build with the
production bb-block descriptor, on a fresh profile with its own HOME and
keychain. Sign-in now reaches **Welcome back** (the test account already
has a linked key). On `main`, the same binary shows *Couldn't sign in*.
I didn't run steps 3–4 below (Create my key, Cancel sign-in) in a real
FI build.

## Before and after

Both runs use the same FI debug build (bb-block descriptor) on a fresh
profile. Only the frontend differs. I cut the browser sign-in step, and
blacked out the account's email and npub.

### Before: `main` (`90ce5e37a`) shows *Couldn't sign in*


https://github.com/user-attachments/assets/3c04ff79-f020-4123-b226-09aeafa15c24

### After: this PR (`373d7484d`) lands on Welcome back


https://github.com/user-attachments/assets/b109f807-7a4a-4dd3-8fef-f199ec76523c

## Human test (FI build, bb-block staging)

1. Build and run with the staging descriptor from
`squareup/buzz-releases` `scripts/build-macos-app-staging.sh`, and use a
fresh profile with no key:
`BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG='<staging descriptor>'
BUZZ_BUILDERLAB_URL= just desktop`
2. Click **Sign in with email** and finish the browser step. You should
land on **Create my key** (or **Welcome back** for an account that
already has a linked key), not *Couldn't sign in*.
3. Click **Create my key**. Setup should link the key and continue. The
app should open without a second organization sign-in prompt.
4. Run it again on a fresh profile and on one already signed in to the
organization. On each, click **Cancel sign-in** during the browser step.
You should return to the landing page with no key created.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <[email protected]>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
These are §8 and the agent.rs memory-read row of §13 from the
23-30 Oct CI flake sweep. Before editing I checked origin/main
(f3446e8) and the open PRs. Neither unit was fixed since the sweep:
no commit on main touches media_spool.rs, media_stream_tests.rs or
the agent test, and no open PR touches media_spool.rs or either test.
Drafts #1461 and #967 rework relay_agent_memories_read, and #1461
also edits media_stream.rs. Neither changes the code paths fixed here.

media_stream_tests.rs:277
a_failure_mid_stream_ends_the_response_short_of_its_length
- CI runs: 39970306754 (job 120470903732), 40829345046 (job
  123299699319) and 40958827040 (job 123745997146). All three fail at
  :300 with 502 where 200 is expected. That is the case of a whole 200
  that sends 2 MiB of a 3 MiB Content-Length.
- Cause: the spool publishes its progress through a tokio watch
  channel, which keeps only the latest value. The copy flushes the
  first MiB as Writing(n), then fails and replaces that value with
  Failed(502). A reader that polls only after both sends sees just
  Failed. read() then returned 502, although the bytes it asked for
  were already on disk, and the response's first block went out as a
  502 head. A player's range request can hit the same race in the app.
- Change: Failed now also carries the last count the copy reported,
  and read() answers from disk whenever that count covers its range.
  Otherwise it returns the failure status, as before. lookup() still
  drops a failed spool, so later reads fetch the video again, and a
  read past the written bytes still fails
  (truncated_whole_videos_fail_and_are_fetched_again is unchanged).
- Verified: a new test, a_failed_spool_still_answers_the_bytes_it_wrote,
  opens a spool on that truncated 200. It first reads the tail, which
  can only return once the copy has failed, and then reads the first
  block. That second read never sees the Writing progress, so the
  ordering is forced rather than timed. Without the media_spool.rs
  change it fails on the first-block read with "called
  `Result::unwrap()` on an `Err` value: 502". With the change it
  passes. :277 and truncated_whole_videos_fail_and_are_fetched_again
  also pass.

agent.rs memory_read_keeps_what_it_decoded_when_its_deadline_passes
- CI run: 40877681973 (job 123460621816), which passed on re-run. It
  failed with 16 entries where 32 are expected.
- Cause: the first case relied on real-clock slack. Each page took
  120 ms against a 300 ms deadline, and page 1 is decoded before
  page 2 is requested, so page 2 only made the deadline if page 1's
  decode finished in about 60 ms. A slow decode on CI pushed page 2
  past the deadline.
- Change: read_through answers each page at once and leaves the
  request after the last page pending, so its fetch parameter is gone.
  The first case now has 32 events, two full pages, so the deadline
  always passes while the third page is awaited. The second case
  asserts the same as before. Its trailing request now pends instead
  of returning an empty page, but by then its deadline has already
  passed.
- Verified: a temporary probe, not committed, injected a 100 ms decode
  stall after page 1 through the helper's stall parameter. The old
  test then failed with "left: 16, right: 32", the CI signature. The
  new test passed. Without the probe, the new test passes.
- Hang guard: with the last request pending, a read that stopped
  honouring its deadline would hang this test rather than fail its
  `took` bound, and CI runs plain cargo test with no per-test timeout.
  read_through therefore bounds the read with a 5 s
  tokio::time::timeout that fails with "memory read outlived its
  deadline". This is a hang guard, not a flake fix; the `took`
  assertions still check the real bound. A temporary probe, not
  committed, set the read's deadline 1000 times later. The test then
  failed after 5.05 s with that message instead of hanging.

Local checks: cargo fmt --check and cargo clippy --all-targets
-D warnings on the Buzz crate, plus the four tests above, run by name.
After the hang guard was added, cargo fmt --check and the memory-read
test were re-run on this snapshot; clippy and the media tests were not.
Deferred: the full Rust suite and hosted CI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 5, 2026
These are §8 and the agent.rs memory-read row of §13 from the
23-30 Oct CI flake sweep. Before editing I checked origin/main
(f3446e8) and the open PRs. Neither unit was fixed since the sweep:
no commit on main touches media_spool.rs, media_stream_tests.rs or
the agent test, and no open PR touches media_spool.rs or either test.
Drafts #1461 and #967 rework relay_agent_memories_read, and #1461
also edits media_stream.rs. Neither changes the code paths fixed here.

media_stream_tests.rs:277
a_failure_mid_stream_ends_the_response_short_of_its_length
- CI runs: 39970306754 (job 120470903732), 40829345046 (job
  123299699319) and 40958827040 (job 123745997146). All three fail at
  :300 with 502 where 200 is expected. That is the case of a whole 200
  that sends 2 MiB of a 3 MiB Content-Length.
- Cause: the spool publishes its progress through a tokio watch
  channel, which keeps only the latest value. The copy flushes the
  first MiB as Writing(n), then fails and replaces that value with
  Failed(502). A reader that polls only after both sends sees just
  Failed. read() then returned 502, although the bytes it asked for
  were already on disk, and the response's first block went out as a
  502 head. A player's range request can hit the same race in the app.
- Change: Failed now also carries the last count the copy reported,
  and read() answers from disk whenever that count covers its range.
  Otherwise it returns the failure status, as before. lookup() still
  drops a failed spool, so later reads fetch the video again, and a
  read past the written bytes still fails
  (truncated_whole_videos_fail_and_are_fetched_again is unchanged).
- Verified: a new test, a_failed_spool_still_answers_the_bytes_it_wrote,
  opens a spool on that truncated 200. It first reads the tail, which
  can only return once the copy has failed, and then reads the first
  block. That second read never sees the Writing progress, so the
  ordering is forced rather than timed. Without the media_spool.rs
  change it fails on the first-block read with "called
  `Result::unwrap()` on an `Err` value: 502". With the change it
  passes. :277 and truncated_whole_videos_fail_and_are_fetched_again
  also pass.

agent.rs memory_read_keeps_what_it_decoded_when_its_deadline_passes
- CI run: 40877681973 (job 123460621816), which passed on re-run. It
  failed with 16 entries where 32 are expected.
- Cause: the first case relied on real-clock slack. Each page took
  120 ms against a 300 ms deadline, and page 1 is decoded before
  page 2 is requested, so page 2 only made the deadline if page 1's
  decode finished in about 60 ms. A slow decode on CI pushed page 2
  past the deadline.
- Change: read_through answers each page at once and leaves the
  request after the last page pending, so its fetch parameter is gone.
  The first case now has 32 events, two full pages, so the deadline
  always passes while the third page is awaited. The second case
  asserts the same as before. Its trailing request now pends instead
  of returning an empty page, but by then its deadline has already
  passed.
- Verified: a temporary probe, not committed, injected a 100 ms decode
  stall after page 1 through the helper's stall parameter. The old
  test then failed with "left: 16, right: 32", the CI signature. The
  new test passed. Without the probe, the new test passes.
- Hang guard: with the last request pending, a read that stopped
  honouring its deadline would hang this test rather than fail its
  `took` bound, and CI runs plain cargo test with no per-test timeout.
  read_through therefore bounds the read with a 5 s
  tokio::time::timeout that fails with "memory read outlived its
  deadline". This is a hang guard, not a flake fix; the `took`
  assertions still check the real bound. A temporary probe, not
  committed, set the read's deadline 1000 times later. The test then
  failed after 5.05 s with that message instead of hanging.

Local checks: cargo fmt --check and cargo clippy --all-targets
-D warnings on the Buzz crate, plus the four tests above, run by name.
After the hang guard was added, cargo fmt --check and the memory-read
test were re-run on this snapshot; clippy and the media tests were not.
Deferred: the full Rust suite and hosted CI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
matt2e added a commit that referenced this pull request Nov 6, 2026
#1970)

## Summary

Fixes two Rust test flakes from the 23–30 Oct CI flake sweep. The first
is a real defect in the media spool, so this PR changes product code.
The second is test-only. This is one of six independent PRs split from
the `flakey-tests` branch. Each is cut from `main` and can merge in any
order.

### A failed spool still answers the bytes it wrote
(`media_stream_tests.rs:277`)

- **CI:** 39970306754 (job 120470903732), 40829345046 (job
123299699319), 40958827040 (job 123745997146).
`a_failure_mid_stream_ends_the_response_short_of_its_length` fails at
:300 with 502 where 200 is expected. The case is a 200 response that
sends 2 MiB of a 3 MiB `Content-Length`.
- **Cause:** the spool reports progress through a tokio `watch` channel,
which keeps only the latest value. The copy flushes the first MiB as
`Writing(n)`, then fails and overwrites that with `Failed(502)`. A
reader that polls only after both sends sees just `Failed`. `read()`
then returned 502, even though the bytes it asked for were already on
disk, and the response's first block went out as a 502 head. A player's
range request can hit the same race in the app.
- **Change (`media_spool.rs`):** `Failed` also carries the last byte
count the copy reported. `read()` answers from disk whenever that count
covers its range, and otherwise returns the failure status as before.
`lookup()` still drops a failed spool, so later reads fetch the video
again. A read past the written bytes still fails, and
`truncated_whole_videos_fail_and_are_fetched_again` is unchanged.

### The memory-read deadline test no longer depends on decode speed
(`agent.rs`)

- **CI:** 40877681973 (job 123460621816) got 16 entries where 32 were
expected. It passed on re-run.
- **Cause:** each page took 120 ms against a 300 ms deadline, and page 1
is decoded before page 2 is requested. Page 2 therefore made the
deadline only if page 1's decode finished in about 60 ms, and a slow
decode on CI pushed it past.
- **Change:** `read_through` answers each page at once and leaves the
request after the last page pending. The first case now has 32 events,
two full pages, so the deadline always passes while the third page is
awaited. The second case asserts the same as before.
- **Hang guard:** because the last request now stays pending, a read
that stopped honouring its deadline would hang instead of failing its
`took` bound, and CI runs plain `cargo test` with no per-test timeout.
`read_through` therefore wraps the read in a 5 s `tokio::time::timeout`
that fails with "memory read outlived its deadline". The `took`
assertions still check the real bound.

### Verification (local macOS)

- The new test `a_failed_spool_still_answers_the_bytes_it_wrote` forces
the ordering. It first reads the tail, which can only return after the
copy has failed, and then reads the first block, so the second read
never sees `Writing`. Without the `media_spool.rs` change it fails on
the first-block read with ``called `Result::unwrap()` on an `Err` value:
502``. With the change it passes. `:277` and
`truncated_whole_videos_fail_and_are_fetched_again` also pass.
- Memory read: a temporary probe (not committed) added a 100 ms decode
stall after page 1. The old test then failed with `left: 16, right: 32`,
the CI signature, and the new test passed. A second probe moved the
deadline 1000× later. The test failed after 5.05 s with the hang-guard
message instead of hanging.
- Ran `cargo fmt --check` and `cargo clippy --all-targets -D warnings`
on the Buzz crate, plus the four tests above by name. After the hang
guard was added, only `cargo fmt --check` and the memory-read test were
re-run. Clippy and the media tests were not.

### Overlaps

Drafts #1461 and #967 rework `relay_agent_memories_read`, and #1461 also
edits `media_stream.rs`. Neither changes the code paths fixed here.

### Remaining gaps

- The full Rust suite has not run locally. Hosted CI is its first full
run.
- A green run does not show that these flakes are gone.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Matt Toohey <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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.

5 participants