Skip to content

[codex] shrink os ssr route bundle#1486

Merged
jonastemplestein merged 10 commits into
mainfrom
feather-mandarin
Jun 11, 2026
Merged

[codex] shrink os ssr route bundle#1486
jonastemplestein merged 10 commits into
mainfrom
feather-mandarin

Conversation

@jonastemplestein

@jonastemplestein jonastemplestein commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

What changed

This PR reduces the OS SSR route graph by moving browser-only and dev-only dependencies behind static SSR/dev branches:

  • Keeps TanStack devtools out of production SSR by moving them to a dev-only lazy module.
  • Stops the shared oRPC client from importing the runtime appRouter; it stays typed from the router but uses the contract/OpenAPI transport at runtime.
  • Loads PostHog, Streamdown, CodeMirror, and ITX TypeScript completion tooling only in browser-only paths.
  • Moves the project stream view lazy import outside the SSR lazy() callback so the server graph has no edge to the stream-view implementation.

Why

The OS server bundle was pulling route/UI dependencies into SSR: TypeScript, Shiki languages/themes, Mermaid/Cytoscape/Katex, CodeMirror/Lezer, PostHog, TanStack devtools, and oRPC handler/domain modules. Several of these came from dynamic imports that were still visible to the server bundler because the SSR guard was inside the lazy callback rather than around the import edge.

Impact

The diagnostic split SSR route graph, with import.meta.env.SSR=true and DEV=false, went from roughly:

  • JS: ~21.1mb
  • sourcemaps: ~67.7mb

To:

  • JS: 3.25mb
  • sourcemaps: 21.9mb

This probe intentionally approximates the route graph with esbuild rather than the full Alchemy/Vite worker build, but it is useful for identifying and comparing dependency edges.

Validation

  • pnpm --dir apps/os typecheck
  • pnpm exec oxlint packages/ui/src/components/source-code-block.tsx packages/ui/src/components/serialized-object-code-block.tsx packages/ui/src/components/posthog.tsx packages/ui/src/apps/providers.tsx packages/ui/src/components/ai-elements/message.tsx apps/os/src/orpc/client.ts apps/os/src/routes/__root.tsx apps/os/src/components/os-devtools.tsx apps/os/src/components/itx-repl.tsx apps/os/src/components/project-stream-view.lazy.tsx --deny-warnings
  • Diagnostic SSR route graph esbuild probe with import.meta.env.SSR=true and import.meta.env.DEV=false

Note: a direct vite build still requires Alchemy's generated local Wrangler config in this worktree.

Environment Config Lease

No active environment config lease.

OS

Status: released
Commit: 3925f2e
Preview: https://os.iterate-preview-4.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-11T10:21:14.117Z

Semaphore

Status: released
Commit: 3925f2e
Preview: https://semaphore.iterate-preview-4.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-11T10:21:02.346Z


Note

Medium Risk
Wide surface area (root providers, markdown rendering, analytics init, editors) but behavior is intended to be equivalent on the client; SSR may briefly show plain fallbacks until lazy chunks load, and PostHog no longer uses React context.

Overview
Shrinks the OS SSR route graph by putting browser-only and dev-only dependencies behind static import.meta.env.SSR / DEV branches so the server bundler no longer follows those import edges (reported diagnostic route JS ~21MB → ~3.25MB).

Shared UI: SourceCodeBlock and SerializedObjectCodeBlock are split into thin SSR shells that lazy-load .client.tsx implementations (CodeMirror, themes, languages). MessageResponse lazy-loads Streamdown on the client with a plain-text SSR fallback. PostHog drops PostHogProvider / setupPosthog at module scope in favor of initPosthog with dynamic import("posthog-js"), called from AppProviders during render.

OS app: TanStack devtools move to a dev-only lazy os-devtools.tsx module. Project stream view uses an SSR stub component instead of guarding inside the lazy() callback. ITX REPL gates TypeScript autocomplete worker deps behind a module-level SSR-null loader.

Adds ImportMetaEnv typings in packages/ui and includes *.d.ts in the UI tsconfig.

Reviewed by Cursor Bugbot for commit 3925f2e. Bugbot is set up for automated code reviews on this repo. Configure here.

@jonastemplestein jonastemplestein marked this pull request as ready for review June 11, 2026 06:16
Comment thread apps/os/src/orpc/client.ts Outdated
Comment thread apps/os/src/orpc/client.ts Outdated
Comment thread packages/ui/src/components/serialized-object-code-block.tsx
Comment thread packages/ui/src/components/posthog.tsx Outdated
jonastemplestein and others added 3 commits June 11, 2026 07:39
Review pass over the SSR-shrink changes, keeping the same server-bundle
wins but with less machinery:

- orpc client: back to the in-process router client on the server. The
  worker serves /api itself (routes/api.orpc.$.ts statically imports
  appRouter), so the router and every domain handler are in the worker
  bundle no matter what client.ts imports. The OpenAPI loopback bought
  zero bytes while adding a self-HTTP hop per SSR call plus cookie and
  base-URL forwarding that had already needed two fixup commits.

- source-code-block / serialized-object-code-block: the implementations
  move verbatim to *.client.tsx with their original static imports and
  full CodeMirror typing; the public modules become the same thin
  SSR-aware lazy wrapper already used by project-stream-view.lazy.tsx.
  This drops the any/unknown type erasure, the per-prop extension
  threading, and the editorReadySignal workaround: with a module
  boundary the editor mounts in the same commit as its parent again, so
  the existing rAF scroll-to-bottom just works.

- posthog: nothing in os/semaphore consumes the PostHog React context,
  so the provider (and the async provider swap, which remounted the
  entire app tree when the fragment was replaced by the loaded provider)
  is gone. PostHogInit is a null-rendering component that lazy-loads
  posthog-js in the browser and calls init; posthog-js/react is no
  longer imported at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4b57528. Configure here.

Comment thread packages/ui/src/components/serialized-object-code-block.client.tsx
React's app-initialization guidance (You Might Not Need an Effect →
Initializing the application) puts once-per-app-load work at module
scope, not in Effects. The api key arrives with loader data so the call
can't literally run at import time; instead the once-guard lives at
module scope and AppProviders kicks it off on the first render that has
config — a plain idempotent function, no Effect, no state, no
component.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jonastemplestein jonastemplestein merged commit a7188c3 into main Jun 11, 2026
13 checks passed
@jonastemplestein jonastemplestein deleted the feather-mandarin branch June 11, 2026 10:20
jonastemplestein added a commit that referenced this pull request Jun 11, 2026
…s at the routing hop, pre-warmed hosts (#1494)

## The problem

A Slack message in prd took **~14s to get the 👀 reaction** and ~20s to
get a reply (example: `iterate` project, thread `ts-1781170058-112929`).
Hop-by-hop, from the message's Slack `ts`:

| Δ | what happened |
|---|---|
| +0.9s | Slack delivered the webhook — Slack was fast |
| **+6.5s** | nothing of ours executed anywhere: cold instantiation of
SlackIntegrationDO + the integration StreamDO (handler: 8.1s wall, **5ms
CPU**). Slack's 3s retry queued behind the same gate and doubled the
work |
| +2.1s | integration DO init + subscription + append + routing |
| **+3.0s** | cold instantiation of the new thread StreamDO |
| **+1.4s** | cold dial of the SLACK_AGENT host DO → input rendered →
eyes at ~14s |
| +6s | LLM leg (openai-ws connect 1.1s, gpt-5.5 ~2s, itx exec) → reply
at ~20s |

Two multiplying causes: **the deployed script was 89.1 MB** (50 MB
sourcemaps + browser-only modules uploaded as worker modules by
alchemy's noBundle glob over `dist/server`; the live server graph is ~34
MB, the entrypoint 1.75 MB) — and every cold DO isolate loads all of it
— times **3–4 distinct DOs chained serially** on the webhook path. The
warm path was always fine (webhook 1–6ms, appends 20–100ms): this is
cold-start tax, not stream-architecture tax.

## The fixes (no change to the streams/processors idea)

1. **`prune-server-bundle.ts`** (runs between build and asset
preupload): deletes every `dist/server` module unreachable from the
entrypoint via import/`new URL` literals (browser web workers + their
wasm that the SSR build emits), plus all sourcemaps **except the
entrypoint's own** (small; the one Cloudflare can symbolicate worker
stack traces with — chunk maps are browser code and pure ballast inside
a worker script). Validated against the extracted prd bundle: keeps
exactly the 186-module live graph, deletes the 3 browser-only modules +
chunk maps.
2. **Append-only webhook ack**: the handler no longer awaits
`SlackIntegrationDO.initialize()` before responding — only the durable
append gates the 200; initialize + catch-up moved to `waitUntil`.
Order-independent (existing integrations have their subscription on the
stream; new ones pick the webhook up via replay). Stops the >3s Slack
retry storm.
3. **👀 at the routing hop**: the slack router reports routed webhooks to
its host (`acknowledgeRoutedWebhook`) and SlackIntegrationDO adds the
reaction immediately — one hop from ingress instead of three cold DO
hops downstream — gated by the same payload-only rules the slack-agent
applies (no bot messages, no reaction events, no bot-user actions).
slack-agent still adds it on catch-up; `already_reacted` makes the pair
idempotent.
4. **Pre-warmed hosts** (`prewarmRoutedStreamHosts`): for a newly routed
thread, the SLACK_AGENT and AGENT host DOs `initialize()` concurrently
with the bootstrap append instead of serially after each dial.
Everything either side appends is idempotency-keyed and
order-independent (the anchor-skip recovery from #1481 covers trigger
ordering).

## Measured

Dev-stage deploys of this branch (`os-dev-jonas`):

- prd today: **89.1 MB**
- this branch pre-#1486 baseline: **34.1 MB**
- this branch on latest main (includes #1486's SSR-graph shrink, 186→178
live modules): **28.3 MB** — 3.1× smaller; app smoke-tested (sign-in
200)
- prune log on the real prd bundle: `kept 186 modules, deleted 3
unreachable modules + 180 sourcemaps (55.0 MB)`

Expected effect: each cold DO instantiation drops from multi-second to
sub-second, and the eyes ack stops depending on the deepest part of the
chain. Worth re-measuring the full message→eyes timing in prd after this
deploys.

## Trade-offs / notes

- Chunk-level deployed stack traces lose symbolication (entrypoint map
kept). Symbolicate locally against the build output if needed.
- The prune is conservative: anything referenced by a quoted relative
specifier (`from`, `import()`, `export from`, `new URL`) stays. The
unreachable set on the real bundle is exactly the browser-only web
workers + wasm.
- Follow-up idea (not this PR): split app-vs-platform workers so UI
deploys stop evicting agent/stream DOs (the 2026-06-10 deploy-race
incident), and consider per-DO-class workers for deploy isolation.

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes production Slack webhook timing, adds best-effort Slack API
calls on the routing path, and alters deploy artifacts via bundle
pruning; behavior is designed to be idempotent but affects a critical
user-visible path.
> 
> **Overview**
> Cuts Slack cold-path latency by shrinking the deployed worker and
parallelizing work on the webhook path.
> 
> **Deploy:** Adds `prune-server-bundle` to the Alchemy build (after
Vite, before asset preupload). It strips unreachable `dist/server`
modules and most sourcemaps so each cold Durable Object isolate loads a
much smaller script.
> 
> **Webhook ingress:** The Slack webhook handler now returns `{ ok: true
}` after the durable stream append only;
`SlackIntegrationDO.initialize()` / `ensureReady()` run in `waitUntil`,
avoiding >3s acks and Slack retries.
> 
> **Routing hop:** `SlackProcessor` gains optional
`acknowledgeRoutedWebhook` and `prewarmRoutedStreamHosts`. The
integration DO adds the 👀 reaction at route time (via
`eyesReactionTargetFromWebhookPayload` + `reactions.add`) and
pre-initializes `SLACK_AGENT` and `AGENT` DOs in parallel with
new-thread bootstrap. Downstream slack-agent behavior stays idempotent
(`already_reacted`).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
8cf05b1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- CLOUDFLARE_PREVIEW -->
## Environment Config Lease
<!-- CLOUDFLARE_PREVIEW_STATE -->
<!--
{
  "apps": {
    "os": {
      "appDisplayName": "OS",
      "appSlug": "os",
      "status": "deployed",
      "updatedAt": "2026-06-11T11:02:23.553Z",
      "headSha": "8cf05b16d08e47333866be25d49508ddcf145a9b",
      "message": null,
      "publicUrl": "https://os.iterate-preview-3.com",
"runUrl": "https://github.com/iterate/iterate/actions/runs/27342005408",
      "shortSha": "8cf05b1"
    },
    "semaphore": {
      "appDisplayName": "Semaphore",
      "appSlug": "semaphore",
      "status": "deployed",
      "updatedAt": "2026-06-11T10:59:58.014Z",
      "headSha": "8cf05b16d08e47333866be25d49508ddcf145a9b",
      "message": null,
      "publicUrl": "https://semaphore.iterate-preview-3.com",
"runUrl": "https://github.com/iterate/iterate/actions/runs/27342005408",
      "shortSha": "8cf05b1"
    }
  },
  "environmentConfigLease": {
    "dopplerConfig": "preview_3",
    "leasedUntil": 1781179095710,
    "leaseId": "699c7e52-ad4d-4c0a-a337-a6b9397144b7",
    "slug": "preview-3",
    "type": "environment-config-lease"
  }
}
-->
<!-- /CLOUDFLARE_PREVIEW_STATE -->
Lease: `preview-3`
Doppler config: `preview_3`
Type: `environment-config-lease`
Leased until: 2026-06-11T11:58:15.710Z

### OS
Status: deployed
Commit: `8cf05b1`
Preview: https://os.iterate-preview-3.com
[Workflow
run](https://github.com/iterate/iterate/actions/runs/27342005408)
Updated: 2026-06-11T11:02:23.553Z

### Semaphore
Status: deployed
Commit: `8cf05b1`
Preview: https://semaphore.iterate-preview-3.com
[Workflow
run](https://github.com/iterate/iterate/actions/runs/27342005408)
Updated: 2026-06-11T10:59:58.014Z
<!-- /CLOUDFLARE_PREVIEW -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant