Repository navigation
feat(client): scheduler requests stats after first paint, with pending, not computed and error states (rows-first c4) - #1027
Closed
paddymul wants to merge 9 commits into
Conversation
…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>
… 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>
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>
…ct (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>
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>
…ability (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>
…ows-first c4) Jest tests for the scheduler (StateOrchestrator rewritten around an IModel: a stats_request after the first infinite_resp, one request per reply until final, nothing after a state change, nothing for a search_string-only change), for the status bar's stats column and Compute summary stats control, for the widget and BuckarooView wiring of that control, and for pinned rows in the error state. Playwright tests on Storybook cover the four states without layout shift and the control's request, and the server spec runs the real standalone page against a session whose first frame says the stats are pending. StateOrchestrator.ts, StatusBar.tsx and BuckarooWidgetInfinite.tsx carry only the signatures the tests use, so the tests fail on assertions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contributor
📦 TestPyPI package publishedpip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37187295169or 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.dev37187295169MCP server for Claude Codeclaude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.9.dev37187295169" --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 |
…tus layout and the control wiring (rows-first c4) Cases found untested or mis-specified after the first push, written against the same stubs: a model that starts complete waits out the delay for its first state change, as does a change made after the earlier stats completed; a request's time is measured once; the Compute summary stats button calls its handler with no arguments; the standalone page offers that button for a session whose stats are not computed and asks for nothing until it is clicked. The layout spec now says what holds across statuses (nothing above or beside the grid moves, and the grid's height follows the pinned area), and the story loads the widget's stylesheet as the real page does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…g, not computed and error states (rows-first c4)
StateOrchestrator becomes the client scheduler for the stats wire. It takes an
IModel, so it sends stats_request {stats_gen, scope: "raw"} through model.send
and watches change:df_meta, change:df_data_dict, change:buckaroo_state and
msg:custom. It asks once the first infinite_resp has arrived (or after 1.5 s
without one), asks again for each reply that leaves df_meta.stats pending, and
stops when the status changes. A change to post_processing, cleaning_method or
quick_command_args waits out a delay of twice the last request's time (200 to
3000 ms) before asking for the next gen; a search_string-only change, or any
other field, leaves the schedule alone. Nothing is requested unless
df_meta.stats says pending.
WebSocketModel starts the scheduler, so BuckarooServerView and the standalone
page get it with no wiring of their own. The status bar gets a fixed-width stats
column, for sessions that report df_meta.stats, showing loading, a Compute
summary stats button that sends stats_request {force: true}, the error reason, or
ready. Valueless pinned rows are omitted in the error state, as in not_computed.
requestStats and StateOrchestrator are exported for hosts with their own IModel.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Oct 4, 2026
paddymul
changed the base branch from
main
to
adr-002-rows-first-stats-delivery
October 6, 2026 14:40
Collaborator
Author
|
Closing. Under the revised D2 of ADR-002 (#1043, 7943412) the server pushes stats after the first row reply, so the client scheduler that sends The status-bar stats cell, the |
paddymul
added a commit
that referenced
this pull request
Oct 6, 2026
…/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>
paddymul
added a commit
that referenced
this pull request
Oct 6, 2026
…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>
This branch was successfully deployed
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.
Problem
A client that advertises
?caps=stats_update(c2, #1025) to a deferred session gets a firstinitial_statewithdf_meta.stats.status == "pending"and merges thestats_updatethat answers astats_request. Nothing sends that request, so the client sits on the stats-free frame with placeholder pinned rows and no host can select deferred delivery yet. The other statuses have no UI either: a session whose stats are not computed, or failed, shows the same empty pinned area as one that is loading, and nothing lets the user ask for stats that were skipped.Phase and plan references
Plan 1 (
plans/01-rows-first-stats-separate-plumbing.md) section 9 "Phase 4", with section 4.0 (client states), section 5 (Strategy C, the client scheduler) and section 2.6 (StateOrchestrator, #810). Plan 2 sections 3 and 4.1 for the request and reply shapes. Message names and shapes follow the server branch #1024.Approach
Scheduler.
StateOrchestrator.tsis rewritten around anIModel. It sendsstats_request {stats_gen, scope: "raw"}throughmodel.sendand watcheschange:df_meta,change:df_data_dict,change:buckaroo_stateandmsg:custom.infinite_resphas arrived, or after 1.5 s if none does (an empty frame, the summary view, a grid that never fetches).StatsChannelgives no reply hook, so a reply is read off the model: a newdf_data_dictunder the samedf_metais astats_updatethat is not final, while a full frame for the same state replaces both and is not taken for one. A status other thanpendingends the chain.post_processing,cleaning_methodorquick_command_argsinbuckaroo_state(the fields the server reruns the dataflow for), or a frame with a newstats_gen. Either one cancels what is scheduled and waits out a delay of twice the last request's time (floor 200 ms, ceiling 3000 ms, 250 ms assumed before one is observed), so a burst of changes asks once, for the state it ends on. The first state a model holds asks without the delay. Asearch_string-only change, and any other field, leaves the schedule alone.df_data_dictbeforedf_meta), so reading at the first event would see the new dict with the old gen.df_meta.statssayspending.stats_genis the server's token, andStatsChannelalready drops replies for another gen, so no client-side token is kept. The scheduler does not sendstate_change:save_changes()already sendsbuckaroo_state_change.Wiring.
WebSocketModelstarts the scheduler (model.scheduler), next tomodel.stats, soBuckarooServerViewand the standalone page get it with no wiring of their own.requestStats(model, {force?})sends the request for the gen the model shows; the scheduler and the control both use it.StateOrchestratorandrequestStatsare exported for a host that supplies its ownIModel.States (plan 1 section 4.0). A session that reports
df_meta.statsgets a fixed-widthstatscolumn in the status bar, after the summary-view selector. It is one line in every status: "Computing summary stats…" with the pulsing dot, a "Compute summary stats" button, "Stats error: ", or "Summary stats ready". The button callson_compute_stats, whichBuckarooViewand the standaloneBuckarooApppass toBuckarooInfiniteWidgetand which sendsstats_request {force: true}. Without that prop (Jupyter) the cell shows "Summary stats not computed" and no button. Sessions with nodf_meta.statsget no column. The pinned area follows c0a: placeholders while pending, and now no row for a valueless key in theerrorstate either, as innot_computed.What changes
src/server/StateOrchestrator.ts(rewritten):StateOrchestrator,requestStats,touchesDataflow,DATAFLOW_STATE_FIELDS.src/server/WebSocketModel.ts(the field andstart()),src/index.ts(two exports).src/components/StatusBar.tsx(StatsStatusCell, the column, the context entry),BuckarooWidgetInfinite.tsx(on_compute_stats),DFViewerParts/gridUtils.ts(erroromits valueless pinned keys),src/style/dcf-npm.css.src/server/BuckarooView.tsxandpackages/js/standalone.tsx: the callback for the control.Tests
Two tests-only commits (
dd6ebe6a,d6afbf98), each pushed and watched on CI before the fix (95dcceec). On both, all 28 entries in the rollup completed and exactly three failed:JS / Build + Test,Server Playwright TestsandStorybook Playwright Tests; everyPython / Testjob passed. On the fix commit all 28 entries completed: 27 succeeded (JS / Build + Test,Server Playwright Tests,Storybook Playwright Testsand all ninePython / Testjobs among them, with the Read the Docs status) anddeploywas skipped. CI logs were not read (REST rate limit); the reasons are from running the same commits locally, where each new test fails on an assertion against the stubStateOrchestrator.tsandStatusBar.tsxof the first commit. The second commit holds cases I found untested or mis-specified after the first push, written against the same stubs (the layout spec first asserted that the grid does not move at all between statuses, which is false when pinned rows are omitted).src/server/StateOrchestrator.test.ts(rewritten, 45 cases): drives a fake model the wayWebSocketModeldrives a real one, plus a block that builds a realWebSocketModelfrom a fake socket. Covers nothing for a session with nodf_meta.statsor a status other thanpending; the first request after the firstinfinite_respand after the timeout; no force flag; one request per reply until final or an error; a full frame for the same state is not a reply (both key orders); the old gen's late reply followed by nothing; each dataflow field cancelling a request waiting out its delay;search_string,df_display,show_commandsandsampledchanges skipped; the delay (floor, ceiling, measured once, after a completed state, for the first change of a model that started complete); resuming for the same state when no frame answers a change;requestStats;touchesDataflow;startandstop.src/components/StatusBar.stats.test.tsx(new, AG Grid stubbed to capture its props): no column and no row field withoutdf_meta.stats; a fixed-width column afterdf_displaywith it; the callback in the grid context; the cell in each status.src/server/BuckarooView.stats.test.tsx(new): the callback sendsstats_request {force: true}for the gen on screen, and nothing without a gen.BuckarooInfiniteWidget.flash.test.tsx(the callback reaches the status bar;erroromits valueless pinned keys),gridUtils.test.ts(the same forextractPinnedRows).pw-tests/stats-scheduler-states.spec.tsandStatsSchedulerStates.stories.tsx(Storybook, added toscripts/test_playwright_storybook.sh): the four statuses with their text, pinned rows and no control outsidenot_computed; nothing above or beside the grid moves between statuses; the control's request.pw-tests/server.spec.ts(server Playwright, additions to "WebSocket data flow"): the real standalone page against a real session whose first frame is rewritten to say the stats are pending, with thestats_requestanswered by the test using the real server's ownall_statsenvelope as the payload (no session the CI server can build is deferred, since that needs xorq): the request comes after the firstinfinite_resp, once, with the first frame's gen, and the pinned rows fill in; anot_computedframe: pinned rows omitted, nothing asked for in 2.5 s, then the button sends the forced request; a default session never sends astats_request.The existing
StateOrchestrator.test.tsis replaced, not extended. It assertedstate_change,compute_stat_groupandstat_group_result, a per-scope token and asend(string)transport, none of which the server handles or the client now has. Cases for the parts that remain (the delay's floor, ceiling and baseline,stop) are carried over in the new file. No other existing test was edited.Run locally on the fix: jest 417 passed (365 before this phase);
tsc -b; all five Storybook Playwright files; the five server Playwright files, 48 passed against a rebuiltstandalone.js;tests/unit1185 passed, 5 skipped (no Python changes). Twenty-four deliberate regressions of the implementation were tried (no paint fallback, a reply read from any dict change, the read not deferred to a microtask, every or no state change counted as a dataflow change, no delay for a later state or a delay for the first, no clamp, no restart on a state change, a request's time measured more than once or never, the first state read at start not marked,forcealways set,stopleaving a listener, the scheduler not started, the status bar column present or absent in the wrong cases, the callback dropped at each of three hops or called with the click event, the error text without its reason,errorkeeping undefined pinned rows), and each makes at least one test fail. Four more were dropped when the code they changed turned out to be dead and was removed.I also ran the built standalone page against a scratch checkout of #1024 with a real deferred
/load_exprsession (a xorq memtable). The sequence wasinitial_state(pending, gen 1),infinite_request,infinite_resp,stats_requestgen 1 five milliseconds later,stats_update(final), the status bar reading "Summary stats ready" and the pinned rows filled in. Three searches 60 ms apart then produced three pending frames (gens 2 to 4) and onestats_request, for gen 4, 200 ms after the last frame. Astats_tier: "schema"session showed the button, and clicking it sentstats_request {force: true}, which #1024 answers withstats_aborted not_requestable, leaving the status as it was. A default xorq session sent nostats_request. That check is not committed because it needs the other branch's server.server-buckaroo-search.spec.ts("searching filters the table data, not just the status bar count") has a race of its own: it reads the grid text as soon as the first.ag-cellis visible, and the pinned cells can be there a few milliseconds before the first data cell. In an interleaved run of 14 cold-server runs each it failed 3 times on this branch's bundle and 2 times on the bundle of the tests-only commit, whose behaviour for a default session is c2's. The job retries twice on CI. It is not changed here.Why default behaviour is unchanged
A session whose server reports no
df_meta.statshas an idle scheduler: it registers four listeners and arms no timer. The status bar has no stats column.extractPinnedRowschanges only for statuserror, which no server sends today. The server Playwright test above asserts that a default session sends nostats_request. A session that ispendingexists only with a deferred/load_expr, which is opt-in.Deviations from the plan
StateOrchestratortakes anIModel; there is no separate adapter. The plan's adapter existed to bridgesend(string)toIModel.send(object), and the class now sends objects through the model directly.state_change,compute_stat_groupandstat_group_resultare gone. The plan's "a merge" is c2'sStatsChannel; the scheduler only sees its effect on the model.WebSocketModel, not byBuckarooViewandstandalone.tsxseparately, so both paths share one construction site. A host that givesBuckarooViewits ownIModelrunsnew StateOrchestrator({model}).start()itself. The two wiring copies carry only the callback for the control.msg:customfor aninfinite_resp(the model emits it once it has paired the response with its parquet frame), not by the grid reporting a paint, with a 1.5 s timeout.erroromits valueless pinned keys; c0a left it behaving ascomplete.not_computedanderroromit the rows, per section 4.0, so the grid is shorter by the pinned area there, and would grow by it if a forced request completes. The status bar and the grid's position and width never change.stats_aborted not_requestable, so it has no effect until plan 3 letsforcerun.Not in this PR
auto_request,requestable,demand_columns,ceiling) and the tier-aware control: plan 3 phase 5. The button is shown for everynot_computedsession whether or not the server could compute.buckaroo-js-corerelease and the tallyman bump. Tallyman pinsbuckaroo-js-core0.15.8, so a host gets the scheduler only after both, and should not selectstats_delivery: "deferred"before then.Stack
Built on
feat/rowsfirst-c2-client-stats-channel(#1025), which itself contains c0a (#1020), so the diff againstmainincludes their commits until they merge: c0a29e1e42c(tests) and405f4ded(fix), c2fe046acc,3118435a,1fd9cff8(tests) and5935432d(fix). This phase's commits aredd6ebe6aandd6afbf98(tests) and95dcceec(fix). Read only those. The server branch #1024 is not part of this PR.🤖 Generated with Claude Code