[codex] shrink os ssr route bundle#1486
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What changed
This PR reduces the OS SSR route graph by moving browser-only and dev-only dependencies behind static SSR/dev branches:
appRouter; it stays typed from the router but uses the contract/OpenAPI transport at runtime.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=trueandDEV=false, went from roughly:~21.1mb~67.7mbTo:
3.25mb21.9mbThis 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 typecheckpnpm 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-warningsimport.meta.env.SSR=trueandimport.meta.env.DEV=falseNote: a direct
vite buildstill requires Alchemy's generated local Wrangler config in this worktree.Environment Config Lease
No active environment config lease.
OS
Status: released
Commit:
3925f2ePreview: https://os.iterate-preview-4.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-11T10:21:14.117Z
Semaphore
Status: released
Commit:
3925f2ePreview: 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/DEVbranches so the server bundler no longer follows those import edges (reported diagnostic route JS ~21MB → ~3.25MB).Shared UI:
SourceCodeBlockandSerializedObjectCodeBlockare split into thin SSR shells that lazy-load.client.tsximplementations (CodeMirror, themes, languages).MessageResponselazy-loads Streamdown on the client with a plain-text SSR fallback. PostHog dropsPostHogProvider/setupPosthogat module scope in favor ofinitPosthogwith dynamicimport("posthog-js"), called fromAppProvidersduring render.OS app: TanStack devtools move to a dev-only lazy
os-devtools.tsxmodule. Project stream view uses an SSR stub component instead of guarding inside thelazy()callback. ITX REPL gates TypeScript autocomplete worker deps behind a module-level SSR-null loader.Adds
ImportMetaEnvtypings inpackages/uiand includes*.d.tsin the UI tsconfig.Reviewed by Cursor Bugbot for commit 3925f2e. Bugbot is set up for automated code reviews on this repo. Configure here.