Commit Graph
5 Commits
Author SHA1 Message Date
a3313f6944 fix(ui): complete read recovery and enforce query error rendering (#18822)
## Thinking Path

> - Paperclip lets people manage agent work through the board.
> - The board reads task, agent, settings, plugin, and run data from the
server.
> - A deploy can interrupt those reads after content is already visible.
> - Local error branches can replace that content with an error or show
raw codes.
> - This pull request moves the remaining reads to the shared recovery
behavior.
> - The query-error guard prevents new local error patterns from
returning.

## Linked Issues or Issue Description

**What happened?**
Loaded lists, settings, task panels, and manual reads could clear their
content or show errors during a server restart. Some reads also disabled
the shared retry policy.

**Expected behavior**
Keep loaded content during a temporary failure. Show the shared
connection banner. Retry reads after recovery. Replace content after a
real access denial or missing resource, with readable copy.

**Steps to reproduce**
Open a task, settings page, or run log. Return 503 with
`tenant_app_unavailable` from the API. Restore the API and check that
content refreshes. Repeat with 403 and 404 to check the failure state.

Related merged work: Refs #15754, Refs #15772, Refs #17960. This change
covers remaining reads and enforcement. Browser persistence and the
outage browser suite follow separately.

## What Changed

- Repair an existing PostgreSQL deadline fixture at UTC boundaries. A
daily cap defers a heartbeat until both its original interval and the
next UTC day allow it. Keep the counted run in its seeded day and cover
noon, just before midnight and the first second after midnight with
explicit clocks and real PostgreSQL. Production scheduling is unchanged.

- Integrate the saved list, settings, shell, task, and manual-read
recovery changes against current master. Preserve the current task,
Inbox, and run-log structure.
- Add `useResilientFetch` for component-local reads. Preserve loaded
data, cancel stale requests, and retry through the shared connectivity
policy. Keep browser and pairing capabilities in memory.
- Retain run log chunks across stream changes and catch up after
recovery without duplicate chunks or stalled requests.
- Keep genuine authorization and missing-resource failures visible.
Translate known single-word codes such as `forbidden`. Show known skills
and disconnected-app failures before sibling loading states. Keep Retry
bound to its failed request.
- Make the query-error guard enforce by default and run it in the CI
policy job. Document the narrow exceptions and the read recovery
contract.
- Invalidate pending OAuth discovery when the operator leaves setup or
its context changes. Keep reserved-popup cleanup bound to the current
attempt.

## Verification

- Current read head `09d1db29dea` is merge-ready: all 59 check runs are
completed (54 successful, three skipped, two neutral), the legacy Snyk
status is successful, and both required `ci / verify` and `ci / e2e`
gates pass. Greptile Apex reviewed this exact head at 5/5 with no
unresolved threads; the branch has no merge conflicts. The current CI
run is `41748339602`.
- Current read head `09d1db29dea` includes master `683fa4725e9`. The
latest change touches only the existing server deadline test fixture.
Application source remains the previously verified `86376fa751e`
revision: all 60 affected skills/disconnected-app tests and both
application UI typechecks passed, along with token gates and the
enforcing query-error guard. Those UI suites were not repeated for this
test-only repair.
- The fixture repair passes all seven deadline integration cases and the
complete server typecheck, including the server test layer. The old
assertion deterministically fails at 23:59:06.888 UTC; its
one-second-old run also fails the first-second-of-day probe. The
corrected fixture passes noon and both midnight probes. The permanent
three-case coverage uses seeded 2099 clocks.
- Twelve new regressions fail against the previous implementation. Both
skills variants and disconnected-app pages show either request's 403 or
429 while the sibling pauses or remains fetching. Permission failures
stay visible. Retry stays usable for its failed request and recovers
independently. An initial test-fixture query-key typo was corrected
before the passing run. The finding is replied to and resolved. Greptile
reviewed prior application revision `86376fa751e` at 5/5 with no new
actionable findings or unresolved threads. The current test-only head
has now received its own Apex 5/5 review with no unresolved threads, and
its CI is successful as recorded above.
- The preceding OAuth fix passed all 237 setup tests on both application
branches. Its ten regressions prove that Back, Cancel, unmount, or
context changes retire initial discovery without creating a connection,
caching old identities, navigating an abandoned popup, or closing a
newer attempt.
- Earlier master integration `e992f770339` passed all 309 tests across
eight affected Slack, recovery, retry, submission and task files, plus
UI typecheck, token gates, enforcing query-error scan and seven guard
tests. The integration preserves Slack workspace reconnect and shared
recovery, readable execution errors, source-inspection company scoping,
and current submission behavior. Newly inherited retained-file recovery
controls keep cached data through transient failures and hide it after
definitive denial.
- The preceding cache head `97c0bc3966f` passed 1,038 focused
cache/write/editor/account/task/API tests. All 22 built-UI
outage/write/loading cases passed in 4.7 minutes on exact browser head
`b7deaaee5da`, with strict checking of all three specs. The latest
skills/app rendering fix is covered by its affected component suites;
those broader cases were not repeated for this separate read path.
- Historical broader UI, CLI and build results and unresolved aggregate
server/DB/adapter failures remain in the PR discussion. Interrupted CI
runs were audited and recorded separately; they are not counted as
passes. Controlled local outages do not prove hosted authentication or a
real Cloud restart/deploy. Real hosted acceptance remains required after
merge and deployment.

Commands for the latest fix:

```sh
pnpm --filter @paperclipai/ui exec vitest run src/pages/CompanySkills.test.tsx src/pages/apps/AppNotConnected.test.tsx
pnpm --filter @paperclipai/ui typecheck
pnpm check:token-gates
node scripts/check-query-error-rendering.mjs --enforce --summary
```

## Risks

This migration touches many read surfaces. The main risks are initial
loading, request cancellation, retained authorization state, and run-log
paging. The tests cover those states. There are no server, schema,
dependency, or API contract changes. The follow-up browser suite and
hosted deploy acceptance are required before release acceptance.

Recovered commits retain their original authors. If squash-merging,
preserve `Co-Authored-By: Bender (Fable) <bender@agents.paperclip.ing>`
and the Paperclip trailer.

## Model Used

OpenAI `gpt-6.1-sol` through Codex, with xhigh reasoning effort, tool
use, and code execution. The context window size is not exposed. Saved
source commits keep their original authorship.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the bug report template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket id
- [x] The affected local tests pass; broader limitations are recorded
above
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Bender (Fable) <bender@agents.paperclip.ing>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 14:07:18 -08:00
73526559b5 fix(ui): reload once when a lazy chunk or its CSS fails to load (#17349)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The board UI loads each page as a lazy chunk. Vite adds a preload
step that also loads the CSS of the chunk.
> - Staging error monitoring reported `Unable to preload CSS for
…/assets/issue-detail-CIbQo9zW.css`. The same file name comes from the
current build, so the CSS did not change. One request for the file
failed for a short time.
> - Vite rejects only the first import that asks for a failed file.
Later imports skip that file. On a task page, the first import is the
task-page prefetch, which ignores errors. The page then shows without
its CSS. On other pages that share the CSS, such as Agents, the lazy
route rejects and the route error boundary shows the error.
> - Neither case recovers without a manual reload. This is a routine
retry that the app can do safely, so the app must do it.
> - This pull request reloads the page one time when a chunk or its CSS
fails to load. A loop guard limits this to one reload in five minutes.
> - The benefit is that a stale tab after a deploy, or one failed asset
request, recovers without an error page, without missing styles, and
without error-monitoring noise. An asset that stays broken still shows
the error and still sends a report.

## Linked Issues or Issue Description

**What happened?**
A board page showed "This page hit an error" with `Unable to preload CSS
for <origin>/assets/issue-detail-<hash>.css`. On a task page, the same
failure gave no error message, but the page showed without its
stylesheet.

**Steps to reproduce**
1. Build the UI and start the server.
2. Make the first request for `/assets/issue-detail-*.css` fail. The new
Playwright spec does this with `page.route(...).abort("failed")`.
3. Open `/<prefix>/agents`. The route error boundary shows "Unable to
preload CSS".
4. Open `/<prefix>/issues/<id>`. The task shows, but the browser does
not request the task CSS again. The page has no task-page CSS.

**Expected behavior**
The app recovers from a chunk or CSS load failure without user action.
If the failure continues, the app shows the error one time, with no
reload loop.

**Environment**
Hosted staging browser. The local build of current `master` reproduces
it.

I searched open and closed PRs for `preload`, `preloadError`, and `stale
chunk`. I found no related PR.

## What Changed

- `ui/src/lib/chunk-load-recovery.ts` (new): `isChunkLoadError` matches
the Vite CSS preload error and the dynamic import errors from Chromium,
Firefox, and WebKit. `reloadForChunkLoadError` reloads one time. A
`sessionStorage` marker blocks a second reload for five minutes. After
the first reload, later chunk errors in the same document are part of
that reload, so they return "reloading" and do not show an error.
Without `sessionStorage`, the function does not reload, because it
cannot guard against a loop. `startChunkLoadRecovery` listens for
`vite:preloadError`. It does not cancel the event, so the import still
rejects as before.
- `ui/src/main.tsx`: Starts the listener before the first render. The
listener covers every import, including the task-page prefetch that
ignores errors.
- `RouteErrorBoundary` and `AppErrorBoundary`: While a recovery reload
runs, they show a loading state. They do not show the error page and do
not report to Sentry. Other errors work as before.
- Tests: Vitest unit tests for the helper and both boundaries. A new
Playwright spec, `tests/e2e/chunk-load-recovery.spec.ts`, runs against
the real server and the built UI.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/lib/chunk-load-recovery.test.ts
src/components/RouteErrorBoundary.test.tsx
src/components/AppErrorBoundary.test.tsx`: 21 tests pass.
- Full UI Vitest suite: 793 files, 9,075 tests pass.
- `pnpm --filter @paperclipai/ui typecheck` passes. `node
scripts/check-token-gates.mjs` is clean. `node scripts/check-maps.mjs`
passes.
- `npx playwright test --config tests/e2e/playwright.config.ts
chunk-load-recovery.spec.ts`: 3 tests pass.
- Task page with one failed CSS request: one reload, the CSS loads, and
the task shows.
- Agents page with one failed CSS request: one reload, the CSS loads,
and the page shows with no error page.
- CSS that always fails: one reload, then the error page shows. There is
no second reload.
- Red proof: I ran the same spec with the `ui/src` changes removed
(current `master` UI code). Both recovery tests fail. On Agents, the
route error boundary shows `Unable to preload CSS for
http://127.0.0.1:3199/assets/issue-detail-CIbQo9zW.css`. On the task
page, the CSS is never requested again.
- A failure I found and fixed during the work: my first version changed
only the error boundaries. The task-page e2e case failed with that
version, because the prefetch received the error and the boundary never
saw it. The `vite:preloadError` listener fixes that case. All results
above are from the final commit.
- The spec blocks service workers, because Playwright page routes cannot
see requests that a service worker sends. With a worker, a failed
network request for an asset that is not cached also gives
`Response.error()`. Thus the preload error is the same.

## Risks

- A page can reload while a user is on it. This occurs only after a
chunk or CSS load failure, and at most one time in five minutes per tab
session. Without this change, that page already shows an error or shows
without styles. Composer drafts use browser storage, so a reload keeps
them.
- A failure that recovers no longer sends a Sentry event. A failure that
continues after the reload still sends one, so a broken deploy stays
visible.
- The error patterns match browser and Vite message prefixes. If a
browser changes its message, that browser goes back to the current
behavior: the error page and a report.
- No API, schema, or server changes.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) by Anthropic, run as a Paperclip
agent through Claude Code with tool use (shell, file edits, Playwright,
and Vitest).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
2026-10-23 12:06:38 -07:00
Devin FoleyandPaperclip fff410dfe7 fix(ui): retain bounded context for browser render errors (#13904)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Its browser error boundaries recover from failed renders and report
the exception when error monitoring is enabled.
> - The boundaries already receive the React component stack, but
discard it before reporting the error.
> - A minified DOM insertion error can therefore lack enough context to
identify the affected component.
> - Browser translation can replace text nodes that React still uses as
insertion anchors.
> - This pull request preserves bounded component names and browser
state for the next failure.
> - Maintainers can locate the failed component without collecting page
content or customer URLs.

## Linked Issues or Issue Description

Related: #13719 attributes browser errors to the loaded release. #13784
supplies the browser environment. This change adds context to
error-boundary reports.

**What happened?**

A browser error boundary reports a DOM `NotFoundError` with only a
minified JavaScript stack. The React component trace is logged to the
console, which production builds remove. Browser translation is a
plausible cause, but the report cannot identify the component or confirm
the translation marker.

**Expected behavior**

An error-boundary report includes a bounded component trace and limited
browser state. It excludes component props, text, HTML, element IDs,
arbitrary CSS classes, page URLs, and query strings.

**Steps to reproduce**

1. Render a component with conditional content before a text node.
2. Replace that text node with a translation element outside React.
3. Enable the preceding conditional content. React tries to insert
before the detached text node and throws `NotFoundError`.
4. The boundary shows its recovery UI, but the old report loses the
component trace.

**Paperclip version or commit**

Based on `6681c71b40`. The regression test reproduces the DOM mutation
with the installed React version.

**Deployment mode**

Built browser UI with optional Sentry monitoring enabled for the
signed-in session.

## What Changed

- Pass the component stack and boundary kind from both error boundaries.
- Keep at most 40 component names from at most 16 KiB of stack input.
Drop locations and unrecognized lines.
- Snapshot document readiness, visibility, and the browser translation
root-class marker before the asynchronous reporting queue runs.
- Attach diagnostics to that event only. Preserve the existing
monitoring gate, sign-out behavior, and original exception if
diagnostics fail.
- Preserve function names in production bundles. Test the actual Vite
production pipeline.
- Document the fields, privacy limits, and translation-marker
limitations.

## Verification

- Focused diagnostics, boundary, real Sentry SDK, and production-build
tests: 49 passed.
- The translation DOM-mutation test reproduces `NotFoundError`, verifies
the failed component trace, and keeps the recovery UI usable.
- The real SDK test checks emitted events, private fixture exclusion,
event isolation, and no capture after sign-out. Its transport stays
in-process.
- `pnpm check:token-gates`: passed.
- [Greptile
review](https://github.com/paperclipai/paperclip/pull/13904#issuecomment-5804220318):
5/5 on `92a2a23d9c`, with no review threads.
- `pnpm -r typecheck` and `pnpm build`: passed.
- Full CI unit and integration test matrix: passed, including all
server, chat, workspace, runner, and serialized suites.
- Local `pnpm test:run` was started, then stopped after the equivalent
full CI matrix passed. The unsharded local run was not completed and is
not claimed as a pass.
- `pnpm exec playwright test --config tests/e2e/playwright.config.ts
tests/e2e/project-repositories.spec.ts --repeat-each=3 --trace=on
--reporter=line`: six tests passed against the production build.
- Initial CI browser shard 8 timed out after the repository form
replaced an enabled Save button with a disabled one before the click.
The retained page snapshot shows the original repository selection. No
error-boundary fallback appeared. [CI attempt
2](https://github.com/paperclipai/paperclip/actions/runs/35929990870/attempts/2)
passed the failed jobs on the same commit. The full PR check matrix is
green. This confirms an intermittent failure, but does not establish the
cause of the first failure.
- Full browser builds with and without name preservation passed. The
initial JavaScript chunk grows from 1,571,919 to 1,668,453 gzip bytes
(+6.1%). Total JavaScript across all chunks grows by 219,610 gzip bytes
(+5.3%).

## Risks

- This adds diagnostics. It reproduces a translation failure mode but
does not identify or repair the specific application component from a
past report.
- The root-class marker is a hint. Other translation tools may omit it,
and its presence does not prove causation.
- Name preservation increases bundle size as measured above. No source
maps are published by this change.
- The error is still reported. DOM operations, browser translation, and
recovery behavior are unchanged.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, code editing, terminal
tools, and test execution. The exact serving model identifier and
context-window size are not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-23 16:23:42 -07:00
Nicky LeachandPaperclip 8f1e3cfe24 feat(observability): add opt-in Sentry error monitoring for the server and the browser (#12190)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server and the browser need clear error reports when an operator
enables external monitoring.
> - Paperclip already uses an opt-in OpenTelemetry pattern for server
traces.
> - Sentry can provide error reports for both runtime paths when the
operator sets one data source name.
> - This pull request adds one opt-in Sentry gate for the server and the
browser.
> - The benefit is faster diagnosis while the default setup sends no
Sentry data.

## Linked Issues or Issue Description

**What is improved?**

Paperclip gains optional error monitoring for server and browser
failures.

**Subsystem affected**

Cross-cutting (server, UI, and shared authentication data).

**Current behavior**

Paperclip has no built-in Sentry error capture for server failures or
browser boundary failures. Operators must inspect local logs and browser
tools.

**Proposed behavior**

When the operator sets `SENTRY_DSN`, the server and authenticated
browser use the same Sentry project. When the variable is absent, both
paths stay inactive. The server loads Sentry dynamically and fails open
when the optional package is absent.

**Reason and benefit**

Operators can inspect runtime errors in one Sentry project. The default
setup remains local and sends no monitoring data.

**Breaking changes**

None when `SENTRY_DSN` remains unset. Authenticated session responses
add the optional `sentryDsn` field.

**Additional context**

The implementation uses built-in Sentry privacy options. It disables
default HTTP context and breadcrumb integrations and keeps
`sendDefaultPii` false.

## What Changed

- Add an opt-in server Sentry gate with dynamic package loading and
fail-open behavior.
- Add the Sentry data source name to the authenticated session response.
- Add an authenticated browser Sentry gate and React error boundary
capture.
- Add tests for server, browser, route, and application error paths.
- Document activation, installation, privacy settings, capture behavior,
and operator controls.

## Verification

- Run `npx vitest run server/src/__tests__/sentry.test.ts`.
- Run `npx vitest run ui/src/lib/sentry.test.ts`.
- Run `npx vitest run server/src/__tests__/auth-routes.test.ts
server/src/__tests__/shutdown.test.ts`.
- Confirm that the full continuous integration suite passes on this pull
request.
- Leave `SENTRY_DSN` unset and confirm that the server and browser gates
stay inactive.
- Set `SENTRY_DSN` and install the optional Sentry packages before a
manual capture check.

## Risks

The operator controls the Sentry project and accepts the data risk when
the operator enables the feature. Error objects can contain messages,
stacks, or cause chains with private values. The default configuration
sends no data because the feature stays off without `SENTRY_DSN`. A
missing optional server package does not stop server boot.

## Model Used

OpenAI Codex, GPT-5, with tool use, repository inspection, GitHub CLI
operations, and code review support.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-26 11:15:45 -07:00
Devin Foley d90f4d488e fix(ui): survive first load against a cold backend without a blank page (#11246)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The web UI holds live websocket connections for run events,
coordinates cross-tab polling through a leader-election store, and
renders app chrome (sidebar, providers) around a routed outlet
> - When the backend is still cold-starting (managed hosting wake,
server restart, reverse proxy up before the app), the event websockets
refuse connections and the first SPA load mounts against a dead backend
> - In that state the mount cascade can exceed React's nested update
limit (minified error #185); the crash originates in shell hooks outside
the routed error boundary, so React unmounts the entire root to a blank
page, and the dead page keeps retrying the websocket on a flat 1.5s
timer until the user hard-refreshes
> - This pull request removes the wasted nested commits from the
shared-polling subscription path, adds exponential backoff to the
transcript websocket reconnect, and adds a last-resort app-shell error
boundary
> - The benefit is that a cold or briefly unreachable backend degrades
to a recoverable state instead of a blank page that hammers the server

## Linked Issues or Issue Description

No existing issue. Description follows the bug template:

**What happened?**

On the first load against a backend that was still starting, the app
showed its loading animation and then a blank page. The console showed
repeated `WebSocket connection to 'wss://…/api/companies/<id>/events/ws'
failed` lines and `Uncaught Error: Minified React error #185` with a
stack through the shared-polling coordinator's `subscribe`. The
websocket retries continued indefinitely on the dead page. A manual
refresh fixed it.

**Expected behavior**

A backend that is briefly unreachable degrades gracefully: websocket
reconnects back off, the UI keeps rendering from cache, and even a
worst-case crash shows a reload prompt instead of a blank page.

**Steps to reproduce**

1. Serve the UI while the backend API is still starting (websocket
upgrades and API calls refused).
2. Load any company page with several shared-polling consumers mounted
(dashboard with sidebar).
3. Observe repeated websocket failures; on affected loads the page goes
blank with React error #185.

## What Changed

- `ui/src/hooks/useSharedPolling.ts`: coordinator snapshot notifications
now keep the previous state object when leadership did not change, so
React bails out instead of scheduling a nested re-render. `subscribe`
invokes its listener synchronously from inside the mount effect with a
fresh object each time; before this change every mount and notify burned
nested-update budget even with no value change — the crash frame in the
field report was exactly this `subscribe → setState` call.
- `ui/src/components/transcript/useLiveRunTranscripts.ts`: the live
event websocket reconnect backs off exponentially (1.5s → 15s cap, reset
on successful open), mirroring `LiveUpdatesProvider`, instead of a flat
1.5s retry.
- `ui/src/components/AppErrorBoundary.tsx` (+ wiring in
`ui/src/main.tsx`): a dependency-free boundary above the router and
providers. `RouteErrorBoundary` only guards the routed `<Outlet />`; a
crash in the shell around it had no boundary, so React unmounted the
root to a blank page. The boundary renders a reload prompt with the
error message.
- Tests: `useSharedPollingSnapshot.test.tsx` (mount costs no extra
commit — fails against the previous code; a real leadership change
re-renders exactly once and ticks stay quiet), a backoff test in
`useLiveRunTranscripts.test.tsx` (delays grow 1.5s → 3s → 6s and reset
after a successful open), and `AppErrorBoundary.test.tsx` (render throw,
effect throw, healthy pass-through).

## Verification

- `pnpm vitest run` in `ui/` over the touched suites (shared polling,
cross-tab poll, transcripts, boundary): 34 tests pass.
- `pnpm typecheck` in `ui/` — clean.
- The snapshot regression test was verified to fail against the
pre-change hook (extra commit per mount).
- Not reproduced end-to-end: the exact 50-update cascade from the field
crash needs a live cold backend; the change removes the identified
per-mount/per-notify nested commits at the reported crash frame, bounds
the reconnect load, and guarantees the shell can no longer blank the
page.

## Risks

Low risk. The snapshot change only suppresses re-renders whose state is
value-identical; leadership changes propagate exactly as before. The
backoff only lengthens retry delays after consecutive failures and
resets on success. The new boundary renders children untouched unless an
error reaches it; behavior on healthy loads is unchanged. Self-hosted
deployments see the same code paths — the cold-backend window simply
rarely occurs there.

## Model Used

- Claude (Anthropic), Claude Fable 5 (`claude-fable-5`), via Claude Code
CLI with extended thinking and tool use (code search, edit, test
execution; diagnosis included mapping the production minified stack to
source).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-08-11 19:16:19 -07:00