Skip to content

fix(islands): retain static output through initial hydration - #10298

Merged
mscolnick merged 1 commit into
marimo-team:mainfrom
peter-gy:ptr/deferred-island-hydration
Jul 23, 2026
Merged

mscolnick merged 1 commit into
marimo-team:mainfrom
peter-gy:ptr/deferred-island-hydration

Conversation

@peter-gy

Copy link
Copy Markdown
Collaborator

Fixes a bug spotted bug in the wild at https://koaning.io/posts/widget-dag/ through mdx-marimo.

The page initially renders exported static output while Pyodide boots. That output contains anywidget model IDs from the export. Plugin registration was trying to resolve those IDs against the live model registry, causing two Model not found errors after the lookup timed out.

before-model-registry-race

The intended lifecycle we'd want is:

  1. Show the exported static output immediately.
  2. Start each island session and run its cells.
  3. Wait for every initial session to report completed-run.
  4. Replace the static output with live output and create model-backed views from the live session.

This PR implements that lifecycle. The bridge records each pending session generation before materializing the island elements, and the worker includes that generation with every kernel message. Static output stays mounted and anywidget view creation waits until the matching initial runs finish. Session generations also keep late messages scoped to the run that produced them.

This way, the static preview is retained with no errors, as long as startup is not completed:

after-static-preview-retained

And once the startup is complete, we have the error-free, interactive live outputs:

after-runtime-interactive

Copilot AI review requested due to automatic review settings July 23, 2026 14:13
@peter-gy peter-gy added the bug Something isn't working label Jul 23, 2026
@vercel

vercel Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Jul 23, 2026 2:14pm

Request Review

Copilot AI 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.

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

@mscolnick

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@mscolnick I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/core/islands/bootstrap.ts">

<violation number="1" location="frontend/src/core/islands/bootstrap.ts:211">
P2: When one session fails to start on a multi-island page, all islands remain on static output forever: the failed generation stays pending because it can never send `completed-run`. Clear failed generations (or track failures separately) when `startSession` is caught so successfully started islands can hydrate.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Browser as Browser
    participant Bootstrap as initializeIslands()
    participant Bridge as IslandsPyodideBridge
    participant Store as Jotai Store
    participant Worker as Pyodide Worker
    participant OutputWrapper as MarimoOutputWrapper (React)
    participant AnyWidget as AnyWidgetPlugin

    Note over Browser,Worker: Page load: static HTML with <marimo-island> elements

    Browser->>Bootstrap: Invoke initializeIslands()
    Bootstrap->>Store: Set islandsPendingInitialRunsAtom = null
    Bootstrap->>Bridge: Start session materialization

    Bridge->>Bridge: parseMarimoIslandApps(root, {materialize: false})
    Note right of Bridge: NEW: inspect only, no element replacement yet
    Bridge->>Store: Set islandsPendingInitialRunsAtom = Set(sessionGenerations)
    Bridge->>Bridge: parseMarimoIslandApps(root) — materialize elements

    Note over Bridge,Worker: Start sessions in worker
    Bridge->>Worker: startSession({code, appId, sessionGeneration})
    Worker->>Worker: Attach sessionGeneration to all kernel messages
    Worker-->>Bridge: Kernel messages with {message, sessionGeneration}

    Bridge->>Bridge: consumeMessages() passes {message, sessionGeneration} to handler

    rect rgba(0,0,0,0.1)
        Note over Bridge,Store: New: completed-run handling
        alt op == "completed-run"
            Bridge->>Store: islandsPendingInitialRunsAtom → remove sessionGeneration
            Store-->>Store: pending set shrinks
        else other ops
            Bridge->>Store: existing handling (ignored)
        end
    end

    Note over OutputWrapper: React rendering of island output
    OutputWrapper->>Store: Read islandsHydratedAtom (pending size === 0)
    alt islandsHydratedAtom == false (still pending)
        OutputWrapper->>OutputWrapper: Show static <marimo-cell-output> children
        Note right of OutputWrapper: NEW: static output retained even if runtime output exists
    else islandsHydratedAtom == true (all initial runs completed)
        OutputWrapper->>OutputWrapper: Show runtime output from notebookAtom
    end

    Note over AnyWidget: AnyWidget plugin (if present)
    AnyWidget->>Store: Read islandsHydratedAtom as canCreateView
    alt canCreateView == false
        AnyWidget->>AnyWidget: Do nothing (skip view creation)
        Note right of AnyWidget: NEW: avoids “Model not found” error
    else canCreateView == true
        AnyWidget->>AnyWidget: Call widgetRegistry.createView(modelId)
    end

    Note over Bridge,Store: Session stop cleanup
    Bridge->>Store: Set islandsPendingInitialRunsAtom = null
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

case "notebook-document-transaction":
return;

case "completed-run":

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.

P2: When one session fails to start on a multi-island page, all islands remain on static output forever: the failed generation stays pending because it can never send completed-run. Clear failed generations (or track failures separately) when startSession is caught so successfully started islands can hydrate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/core/islands/bootstrap.ts, line 211:

<comment>When one session fails to start on a multi-island page, all islands remain on static output forever: the failed generation stays pending because it can never send `completed-run`. Clear failed generations (or track failures separately) when `startSession` is caught so successfully started islands can hydrate.</comment>

<file context>
@@ -201,6 +208,17 @@ function handleMessage(
       case "notebook-document-transaction":
         return;
 
+      case "completed-run":
+        store.set(islandsPendingInitialRunsAtom, (pending) => {
+          if (!pending?.has(sessionGeneration)) {
</file context>

@mscolnick
mscolnick merged commit e17dd29 into marimo-team:main Jul 23, 2026
29 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.15-dev65

williambdean pushed a commit to williambdean/marimo that referenced this pull request Sep 6, 2026
…team#10298)

Fixes a bug spotted bug in the wild at
https://koaning.io/posts/widget-dag/ through mdx-marimo.

The page initially renders exported static output while Pyodide boots.
That output contains anywidget model IDs from the export. Plugin
registration was trying to resolve those IDs against the live model
registry, causing two `Model not found` errors after the lookup timed
out.

<img width="1440" height="1000" alt="before-model-registry-race"
src="https://github.com/user-attachments/assets/422bf2e8-2ac9-4b1b-9a4f-ddd1361a4d50"
/>

The intended lifecycle we'd want is:

1. Show the exported static output immediately.
2. Start each island session and run its cells.
3. Wait for every initial session to report `completed-run`.
4. Replace the static output with live output and create model-backed
views from the live session.

This PR implements that lifecycle. The bridge records each pending
session generation before materializing the island elements, and the
worker includes that generation with every kernel message. Static output
stays mounted and anywidget view creation **waits until the matching
initial runs finish**. Session generations also keep late messages
scoped to the run that produced them.

This way, the static preview is retained with no errors, as long as
startup is not completed:

<img width="1440" height="1000" alt="after-static-preview-retained"
src="https://github.com/user-attachments/assets/21f6925a-9696-4ba5-b4dd-62c694b0c2c4"
/>

And once the startup is complete, we have the error-free, interactive
live outputs:

<img width="1440" height="1000" alt="after-runtime-interactive"
src="https://github.com/user-attachments/assets/03795cf6-e449-4957-aa89-2314210f3e3a"
/>

This branch was successfully deployed

1 active deployment
Preview — 35f2b927 Deployed Jul 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants