Skip to content

fix: Drop held changes to models the client has not been sent - #8906

Open
philippjfr wants to merge 1 commit into
mainfrom
fix/hold-combine-unsynced
Open

philippjfr wants to merge 1 commit into
mainfrom
fix/hold-combine-unsynced

Conversation

@philippjfr

@philippjfr philippjfr commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Description

Changes made inside a hold() with the default 'combine' policy can produce a patch the client cannot apply. The browser then rejects the whole patch with DeserializationError: reference pXXXX isn't known, and none of the held changes appear.

The combine policy merges a later change to a property into the slot of the earlier event for the same property. If the earlier event was the only one that defined a new model, and a change to that model was recorded in between, the change ends up ahead of any definition. A concrete case is two HoloViews plots with a shared range that re-render twice in one hold:

  1. The first re-render sets holder.children to a figure that defines a new Range1d.
  2. The range's start changes.
  3. The second plot links to the range (other.x_range = range, which only references it).
  4. The second re-render sets holder.children again. This is combined into slot 1, so slot 1 no longer defines the range, and slot 2 now references a model the client does not have.

This change drops held changes to models the client has not been sent yet (Bokeh's doc.models._new_models), both in _flush_events and in the threaded unhold. These changes are redundant, because the message that first defines a model serializes its current state.

This was found while debugging Lumen's ComponentController (holoviz/lumen#2056), which writes several widgets inside one hold(). On the penguin copilot example, the agent's tool call succeeded on the server, but nothing changed in the browser.

Minimal reproducer (click go, then check the browser console):

import asyncio
import hvplot.pandas
import pandas as pd
import panel as pn
import panel_material_ui as pmui
from panel.io.document import hold

df = pd.read_csv("https://datasets.holoviz.org/penguins/v1/penguins.csv").dropna()
species = pmui.MultiChoice(label="Species", options=sorted(df.species.unique()), value=sorted(df.species.unique()))
x = pmui.Select(label="X axis", options=["bill_length_mm", "flipper_length_mm"], value="bill_length_mm")
color_by = pmui.Select(label="Colour by", options=["species", "sex"], value="species")
data = pn.bind(lambda s: df[df.species.isin(s)], species)

async def run(event):
    await asyncio.sleep(0.2)
    with hold():
        species.value = ["Gentoo", "Chinstrap"]
        color_by.value = "sex"
        x.value = "flipper_length_mm"

go = pmui.Button(label="go", on_click=run)
pmui.Row(
    pmui.Column(species, x, color_by, go),
    pmui.Column(
        pn.bind(lambda d, x, c: d.hvplot.scatter(x=x, y="body_mass_g", by=c, responsive=True, height=300), data, x, color_by),
        pn.bind(lambda d, x, c: d.hvplot.hist(y=x, by=c, responsive=True, height=200), data, x, color_by),
        sizing_mode="stretch_width",
    ),
).servable()

Before this change, the browser logs reference … isn't known and keeps the old widget values. After it, all three changes apply.

Tests: test_combined_patch_references_unsent_model reproduces the undecodable patch with plain Bokeh models and Bokeh's own Deserializer. test_drop_unsent_model_changes_keeps_combined_patch_decodable checks that the filtered patch decodes. All of panel/tests/io/test_document.py passes locally.

AI Disclosure

Tool & Model: Claude Code + Claude Opus 5.5
Usage: Debugged the issue from the Lumen copilot down to Bokeh's event combining, wrote the fix and tests, and drafted this description.

  • I have tested all AI-generated content in my PR.
  • I take responsibility for all AI-generated content in my PR.

Checklist

  • Tests added and are passing

Under the combine hold policy a later change is merged into the slot of an
earlier one, which can discard the only definition that preceded a change to
a new model, e.g. a range shared between two plots that re-render twice in one
hold. The client then rejects the whole patch with "reference isn't known".

Assisted-by: Claude Code:claude-opus-5-5

This branch has not been deployed

No deployments
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