Skip to content

docs(adr): ADR-002 rows-first delivery with server-pushed summary stats - #1043

Merged
paddymul merged 12 commits into
mainfrom
adr-002-rows-first-stats-delivery
Oct 6, 2026
Merged

paddymul merged 12 commits into
mainfrom
adr-002-rows-first-stats-delivery

Conversation

@paddymul

@paddymul paddymul commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The rows-first stack changes how a session delivers summary stats: a stats-free first message, a typed stats_update keyed by a server-owned stats_gen, and stats that follow the first rows. The decisions behind it are spread across the descriptions of a dozen PRs, several of them now closed, and a set of design notes outside the repo. There is no one document to review the design against.

Change

Adds docs/plans/ADR-002-rows-first-stats-delivery.md, status Proposed. It records eight decisions (typed stats_update, a server push after rows, stats_gen keying, the schema tier as a dataflow level, final assignment, legacy-client behavior, search and state changes, telemetry), the alternatives rejected, the consequences, and what is left to ADR-001, ADR-003 and a scan_parquet backend design (now ADR-004, #1059).

D2 was revised after review. The first draft had the client pull stats with stats_request and the server run resumable units for a bounded time per request. The ADR now assumes ADR-001's cache works, and the server pushes stats to a connection once the write future of that connection's first row reply resolves, following the ADR-003 stats policy. The unit and time-budget decisions are gone, and cold stats are listed under "Not decided here".

The branch also carries the client half of the wire, #1025, which was merged into it. StatsChannel key-merges stats_update into all_stats and drops an update for any other stats_gen, and WebSocketModel, BuckarooServerView and packages/js/standalone.tsx add caps=stats_update to the WebSocket URL. Merging this PR lands that code on main. It has no effect against the server on main, which ignores the query string in open() and never sends stats_update. The default stays inline.

Stack

As of 2026-10-06, under the revised D2:

Merging this PR and deleting its branch retargets the open PRs above to main. The branch name has no slash because the Checks workflow's pull_request trigger uses branches: "*", which does not match a base branch containing a slash.

Companion ADRs: ADR-001 (#1040, merged; the stat cache this ADR assumes), ADR-003 (#1044, which stats a session runs at a given size) and ADR-004 (#1059, the scan_parquet backend that rows-first delivery waits on).

🤖 Generated with Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

📦 TestPyPI package published

pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37519133609

or with uv:

uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37519133609

MCP server for Claude Code

claude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.9.dev37519133609" --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo-table

📖 Docs preview

🎨 Storybook preview

paddymul and others added 5 commits October 6, 2026 14:22
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The client-pull design (stats_request after first paint, resumable
units, per-connection cursors, a per-request time budget) is replaced
by a server push. The server sends a connection its stats only after
the write future of that connection's first row reply resolves, and
pushes the tier the ADR-003 stats policy runs automatically. The ADR
now assumes ADR-001's cache, so the unit and time-budget decisions
(old D5 and D6) are dropped and cold stats move to "Not decided here".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ability (rows-first c2) (#1025)

* test(client): failing tests for client hardening under a two-message protocol (rows-first c0a)

Tests for the client half of plan 1 phase 0a. A server that sends the
summary stats separately, or not at all, leaves the first message
without values the client has been assuming.

BuckarooView: a change that reached the model before the effect
subscribed, one initial_state with metadata decoding once, and
out-of-order decodes applying the newer.

Pinned rows: a valueless key shows a placeholder with its own row id
while df_meta.stats.status is pending, and is omitted when not_computed.
The simple tooltip returns nothing for a valueless cell. color_map is
silent without bins and restyles when they arrive. An unrelated
df_data_dict update keeps the in-flight indicator when the server
reports df_meta.stats.

One Storybook Playwright test covers pending, not_computed and complete
in a real browser, and is added to the Storybook list in
scripts/test_playwright_storybook.sh.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(client): harden the client for a two-message protocol (rows-first c0a)

BuckarooView reads model.get() for every key once its listeners are in
place, so a change:* emitted before the effect ran reaches React. All
df_data_dict values go through one decoder (makeLatestDictDecoder) that
skips a dict it has already seen and applies only the newest decode, so
an initial_state with metadata decodes once and out-of-order decodes
cannot overwrite a newer one. standalone.tsx gets the same decoder and
catch-up.

df_meta.stats.status reaches the grid as stats_status. While pending, a
required pinned key with no value becomes a placeholder row that carries
its key as index (unique row id, empty value cells). When not_computed
it is omitted. A missing df_meta.stats, or any other status, keeps the
old behaviour.

The simple tooltip returns nothing for a cell with no value or no row
data. color_map no longer logs when bins are missing, and the grid
refreshes the color-mapped columns when their bins change after the first
render. That needs RenderApiModule, which was not registered, so
api.refreshCells logged AG Grid error 200 and did nothing.

BuckarooInfiniteWidget keeps inFlight when only df_data_dict changed and
the server reports df_meta.stats; the answer to the dispatched change is
a frame with a new df_meta. Without df_meta.stats the rule is unchanged.

The Playwright story test no longer hovers a valueless pinned cell: with
the old tooltip it still passed, since AG Grid does not call the tooltip
for an empty value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(client): failing tests for the stats_update channel (rows-first c2)

Jest tests, driven through a WebSocketModel with a fake socket, for the
client half of the stats wire: a stats_update is key-merged into all_stats
and dropped when its stats_gen is not the expected one, the expected gen
follows df_meta.stats.gen on every applied initial_state, a merge in
flight is discarded or redone when a frame replaces the dict, and
stats_aborted moves the status. Plus withStatsCapability and
BuckarooServerView putting ?caps=stats_update on the WebSocket URL, and a
server Playwright test that the standalone page does the same.

StatsChannel.ts is a stub (identity withStatsCapability) so the tests fail
on assertions rather than on a missing module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(client): failing tests for stats_update payloads and an empty dict (rows-first c2)

Three more cases for the stats_update merge, found untested after the
first test commit: a wide parquet_b64 payload as the server sends it (the
shared summary_stats fixture, not a json envelope), a model that holds no
df_data_dict yet, and a dict with no all_stats key.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(client): failing test for stats_update ordering (rows-first c2)

Two stats_update messages for one gen are applied in arrival order even
when the first payload decodes more slowly: the final update merges last
and the status completes only after both merges.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(client): merge stats_update into all_stats and advertise the capability (rows-first c2)

StatsChannel is the client half of the stats wire. WebSocketModel hands it
every text frame first. A stats_update for the gen the model's df_meta
reports is decoded and key-merged into df_data_dict.all_stats (new row
objects, a new dict), updates apply one at a time in arrival order, and a
final update sets df_meta.stats to complete at the update's tier. A merge
is dropped when the gen moves on while it decodes and redone when a frame
replaces the dict. stats_aborted for the expected gen sets error or
not_computed. Other message types are ignored as before.

withStatsCapability adds caps=stats_update to a WebSocket URL; both wiring
copies (BuckarooServerView and the standalone page) use it so the server
treats them as capable clients.

The guard tests that already pass before the fix (caps already present, no
df_meta.stats, ignored aborts, unknown types, infinite_resp pairing) are
added here, with a makeModel(null) helper so a model without stats can be
built.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(client): Storybook + Playwright for the stats_update channel (rows-first c2)

StatsUpdateChannel renders a real WebSocketModel through BuckarooView in
buckaroo mode on a fake in-page server: infinite_request gets an
infinite_resp JSON frame plus a binary frame, a buckaroo_state_change (the
search box) gets an initial_state carrying the next stats gen with pending,
schema-only stats. stats_update frames go out only on a button, one pair per
gen, so the tests choose their order against the other frames. Each gen's
mean row comes in two column chunks, age then score (final), each padding the
other column with null as the wide pivot does.

stats-update-channel.spec.ts:
- the age chunk fills its pinned cell and leaves the gen pending; the final
  score chunk fills score, keeps age (its null does not overwrite) and the
  schema dtype row, and completes df_meta.stats.
- after a search moves the client to gen 2, both of gen 1's chunks are
  dropped; gen 2's chunks merge the filtered rows' means.

Checked against StatsChannel mutations: ignoring stats frames or skipping
the status change fails both tests, removing the gen checks fails the
search test, letting a null overwrite fails the chunk test. Added to the
Storybook list in test_playwright_storybook.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… s1) (#1021)

* test(dataflow): characterize widget trait order and merged_sd before the _handle_widget_change split (rows-first s1)

A default-tier XorqBuckarooWidget and BuckarooWidget publish df_data_dict,
then df_display_args, then the rest of the widget_args_tuple observers on a
search change, and merged_sd carries the full stat set. These pass on main
and pin the behaviour the split must keep.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(xorq): failing tests for the xorq schema stats tier and its load-handler fields (rows-first s1)

A schema-tier XorqServerDataflow should match full stats on pinned_rows,
data_key, summary_stats_key and (except the stats-derived minWidth)
column_config, issue no data query besides the cached count, and keep
init_sd hints and sorted windows working. A pending state must write
nothing under a full-tier cache key, and a later full assignment must reach
merged_sd for the raw, clean and filt scopes. assemble_merged_sd must equal
merged_sd, and _handle_widget_change must be built from separately callable
all_stats and display-args builders. /load_expr and /reload_expr accept
stats_tier and stats_delivery, replay them on reload, and keep them out of
the warm short-circuit's has_config tuple.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(xorq): stats tier machinery and the xorq schema tier (rows-first s1)

Add a dataflow-level stats_tier ("full" default, "schema"). The xorq schema
tier builds identity and typing for every column from the expression's
schema, with no data query beyond the cached row count, so a dataflow
constructs in milliseconds rather than the stats' hundreds.

The tier is part of _scope_cache_key, so a schema entry is never read as a
full one, and _populate_sd_cache stores summary_sd under the filt key only
if it was computed for the current frame, klass list and tier. add_analysis
no longer builds DFStatsClass outside the hook when the tier is not full.
The merged_sd observer body is extracted as the pure assemble_merged_sd,
and _handle_widget_change is split into _build_df_data_dict and
_build_df_display_args.

/load_expr and /reload_expr accept stats_tier and stats_delivery, stored on
the session beside dataflow_kwargs and replayed on reload. They stay out of
the has_config tuple; the warm short-circuit compares the stored pair.
stats_delivery="deferred" builds the schema-tier dataflow and publishes it.
Defaults (full, inline) leave behaviour unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(dataflow): accept pandas 3's str dtype in the pandas characterization test (rows-first s1)

The Max Versions jobs resolve pandas 3, which reports a string column's
dtype as 'str' where pandas 2 says 'object'. The characterization test
asserted 'object'. Verified in a Max Versions environment (pandas 3.0.6,
polars 1.44.2, xorq 0.4.5): the unit suite passes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(xorq): failing test for the schema tier overriding init_sd typing on a skipped column (rows-first s1)

A column in skip_stat_columns gets only name, dtype and length from the
full-tier pipeline, so its _type comes from init_sd. The schema tier layers
the schema-derived _type and is_* keys over it, so an int64 column that
init_sd types as float merges as integer and renders with zero fraction
digits instead of the float displayer init_sd asked for.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(xorq): schema tier leaves a skipped column's typing to init_sd (rows-first s1)

_get_schema_sd never read skip_stat_columns, so a skipped column's
schema-derived _type and is_* keys overrode init_sd's _type once merged. The
full tier gives a skipped column only name, dtype and length. The schema tier
now does the same, so init_sd's _type decides the displayer at both tiers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(xorq): failing test for a schema-tier add_analysis that fails validation (rows-first s1)

At the schema tier, add_analysis assigns the new klass list before
verify_analysis_objects runs, so a klass that fails validation stays
installed and every later add_analysis raises.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(xorq): schema-tier add_analysis validates the klass list before assigning it (rows-first s1)

A klass that fails verify_analysis_objects no longer stays in
analysis_klasses, so later add_analysis calls and a switch to the full
tier keep working. Both tiers now validate the candidate list and only
then assign it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(server): failing test for /reload_expr with a body that isn't UTF-8 (rows-first s1)

_optional_body catches JSONDecodeError and TypeError, but json.loads on
bytes that aren't valid UTF-8 raises UnicodeDecodeError, so the reload
returns 500 where a body it can't parse should be ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(server): /reload_expr ignores a body that isn't UTF-8 (rows-first s1)

_optional_body catches ValueError, which covers both JSONDecodeError and
the UnicodeDecodeError json.loads raises for non-UTF-8 bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(dataflow): failing tests for the stats_tier trait, set_stats_tier, add_analysis and /load (rows-first s1)

- Assigning stats_tier changes nothing: it is a plain attribute, so a schema
  dataflow switched to full keeps its schema stats, and any string is accepted.
- set_stats_tier, which installs a summary computed elsewhere (or one an
  earlier visit cached) without computing the filtered scope again, does not
  exist. The two tests that assigned summary_sd after flipping the tier now
  go through it or the trait.
- add_analysis never reruns the summary stats, so an added stat never
  reaches merged_sd.
- At the schema tier, an op under a search counts the rebuilt clean-scope
  frame as well as the filtered one.
- /load leaves the stats policy on the session, so a later /load_expr that
  omits it inherits an earlier load's deferred delivery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(dataflow): stats_tier is a trait, set_stats_tier installs stats in hand, add_analysis reruns the summary (rows-first s1)

stats_tier is now a traitlets Enum that _summary_sd observes. Assigning it
validates the value and computes the new tier's stats, so switching tiers no
longer means assigning summary_sd by hand. set_stats_tier switches without
that compute when the stats are already in hand: a summary computed elsewhere
(stored under the state's key at the new tier), or the entry an earlier visit
cached. It records the dedupe key first, so the observer doesn't redo them.

_summary_sd records its dedupe key only after the stats are computed. The
_populate_sd_cache guard no longer computes the filt scope itself: a
summary_sd that isn't this frame's is left out of the cache, and the next run
fills it. That removes the synchronous recompute on a tier switch and the
errs it dropped.

add_analysis builds the klass list with with_stat, the rule both pipelines'
add_stat now share, validates it, then reruns _summary_sd. The full-tier
branch used to run two stat pipelines and throw both away, so an added stat
never reached merged_sd.

The tier is checked once, in CustomizableDataflow._get_summary_sd, which
dispatches to _get_full_sd or _get_schema_sd. XorqDataflow overrides
_get_full_sd and keeps only its pandas-frame branch in _get_summary_sd. Its
_get_schema_sd takes the scope and gives the clean scope no length: that
frame is rebuilt on every cache miss, so its count was a query of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(server): /load resets the session's stats policy, and its defaults are defined once (rows-first s1)

/load clears the xorq state a /load_expr left but kept stats_tier and
stats_delivery, which /load_expr reads as the defaults for a re-POST that
omits them. A /load in between therefore handed an earlier load's deferred
delivery to the next /load_expr. /load now resets the pair.

The new-session defaults were hard-coded in LoadExprHandler as well as on
SessionState. Both now read DEFAULT_STATS_TIER and DEFAULT_STATS_DELIVERY
from session.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: the infinite widget reuses the dataflow's builders, and test imports move to module level (rows-first s1)

BuckarooInfiniteWidget._handle_widget_change repeated the df_data_dict and
df_display_args assembly this PR moved into _build_df_data_dict and
_build_df_display_args. It now calls them. Its dataflow is built with
skip_main_serial, so 'main' stays empty as before.

assemble_merged_sd, Expr and XorqStatPipeline were imported inside test
functions, against the repo's rule. They are now module-level imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@paddymul
paddymul force-pushed the adr-002-rows-first-stats-delivery branch from c500780 to f61df3d Compare October 6, 2026 18:27
…ed sessions (rows-first s3) (#1024)

* test(server): failing tests for the stats wire format and whole-run stats_request (rows-first s3)

On a deferred /load_expr session a stats_request {stats_gen, scope} should
return a stats_update with the matching stats_gen whose inline wide payload
equals the all_stats an inline session sends, a stale stats_gen should get
stats_aborted and run no query, and /load_expr and /reload_expr should bump the
generation. df_meta.stats should be injected on every frame and survive a
dataflow-field change, which returns the session to the schema tier. With a
caps client and a legacy client on one session, the legacy client should keep
getting complete messages through the websocket broadcast, the /load_expr,
/reload_expr, /load and /load_compare pushes and the highlight overlay, while
the caps client gets a stats-free frame and then pulls a stats_update. A spy
telemetry sink should see a stats.request span.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing tests for stats reuse, component_config and generation edge cases (rows-first s3)

Four more cases for the stats wire format, kept in their own commit so each is
seen failing on CI before the implementation lands. Returning to a state whose
stats were completed once is answered from summary_stats_cache with no query.
Completing the stats keeps the session's component_config on the refreshed
display config. A warm /load_expr, which rebuilds nothing, leaves stats_gen
alone. A stats_request on a session with no data is answered with
stats_aborted.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3)

A client that advertises ?caps=stats_update gets a stats-free initial_state on a
deferred /load_expr session (df_meta.stats.status "pending") and pulls the stats
with stats_request {stats_gen, scope}. The reply is a stats_update carrying the
dataflow's all_stats as an inline wide envelope, or stats_aborted when the
generation is stale. The request is the whole run: one synchronous call that
computes the full stats, writes the full-tier summary_stats_cache entry, assigns
summary_sd and refreshes the session snapshot through one helper.

stats_gen is a server-owned counter bumped by every load handler and by a state
change that touches a dataflow field, which also returns a deferred session to
the schema tier. df_meta.stats is injected by build_state_message from the
session, since the dataflow rebuilds df_meta wholesale. Every send site goes
through build_state_message_for, so a client without the capability still gets
complete messages (its missing stats run synchronously first); broadcast_state
replaces the five copies of the send loop and sends to capable clients first.
The stats_request branch binds the session's telemetry sink and emits a
stats.request span.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing tests for errs on a stats cache hit, the summary dedupe key, span sinks and a failed state change (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(server): set_stats_tier keeps errs per cache entry and rolls back; stats.complete span; failed state change restores the tier (rows-first s3)

- A summary_stats_cache hit no longer reports a clean run: the errs of the run that filled an entry are kept beside it (_summary_errs_cache) and set_stats_tier reads them back. errs is assigned before summary_sd so _populate_sd_cache files them with the sd.
- set_stats_tier puts the tier and the summary key back when the switch raises.
- complete_stats times the run as stats.complete rather than firstpull.stats_total, which also fired for completions long after the load, and binds the session's telemetry sink only when it has one so an outer sink is not replaced by None.
- A buckaroo_state_change that raises puts the dataflow's tier back, so it matches the snapshot the session still describes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): the stats wire tests follow main's send path and the stats.complete span (rows-first s3)

The legacy overlay test calls _send_client_state, which replaced _send_highlight_overlay on main, the span test expects stats.complete, and the sink test only rules out firstpull.stats_total (the xorq dataflow emits its own firstpull.summary_stats).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): e2e check that eager stats reach the DOM on /load_expr (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing e2e check that deferred stats reach the DOM on /load_expr (rows-first s3)

The page renders its rows and the schema-tier dtype row, and the stats rows stay empty: no client sends stats_request after the first frame (the scheduler PR #1027 is closed) and the server pushes nothing (ADR-002 D2 is unimplemented).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(server): failing tests that deferred stats are pushed after the first row reply (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(server): push stats after the first row reply on a deferred session (rows-first s3, ADR-002 D2)

A client that advertises ?caps=stats_update and is sent a pending frame is owed
that generation's stats. The handler registers a continuation on the write
future of the client's first row reply; it sends the stats_update once the rows
are on the socket, if the connection is still open and the generation is still
current. The stats run once per session, so a later connection's push is
answered from the session. The stats.push span records the gap since the rows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
paddymul and others added 4 commits October 6, 2026 15:11
set_stats_tier unpacked the Dict trait into a literal, which basedpyright types as Any with no mapping; it copies with dict() as _populate_sd_cache does. XorqServerDataflow's scope-chain lookup passed the Optional merged_operations to split_chain_by_scope.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er state change, display config on the final update and a push keyed to the owed generation (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…iting on a timeout (rows-first s3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nge, display config on the final update, push keyed to the owed generation (rows-first s3)

A deferred session whose stats run failed goes back to pending when the next connection opens, so a reload retries it. A state change on a completed session holds trait notifications and assigns the tier last, so the cascade runs once for the new state, and a failed change no longer reruns full stats to roll the tier back; the tier a stats_update reports is the session's. The final stats_update carries df_display_args when the stats changed them, and StatsChannel applies it. infinite_request carries the client's stats_gen, and only the reply to a request for the owed generation starts the push. Drops the unused STATS_STATUSES and moves the component_config merge into apply_component_config for /load_expr and the snapshot refresh.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ull, behind a test-only 1 s delay (rows-first s3)

BUCKAROO_TEST_STATS_DELAY_S makes complete_stats wait before the full stats run. The server Playwright config sets it to 1, so the summary view is read while only the schema tier has arrived. With it set to 0 the empty check fails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rid's column definitions (rows-first s3)

A jest test drives a real WebSocketModel and the buckaroo widget with AgGridReact stubbed: a final update's df_display_args moves the column's minWidth from 100 to 135 and does not remount the grid. The server e2e also asserts the update's minWidth is larger than the pending frame's. The grid's column widths are not observable in the browser (columns are not resizable and keep their sized width), so the DOM is not asserted.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@paddymul
paddymul added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit d6d58a1 Oct 6, 2026
28 checks passed

This branch was successfully deployed

1 active deployment
testpypi — 029bbb6c Deployed Oct 6, 2026 by paddymul via Publish to TestPyPI #1813
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