Skip to content

fix(py): let server(data_source=) survive a second session - #302

Merged
cpsievert merged 1 commit into
fix/py-server-data-source-parityfrom
fix/py-server-guard-bypass
Sep 12, 2026
Merged

fix(py): let server(data_source=) survive a second session#302
cpsievert merged 1 commit into
fix/py-server-data-source-parityfrom
fix/py-server-guard-bypass

Conversation

@cpsievert

Copy link
Copy Markdown
Contributor

Stacked on #301

Depends on #301 (restore server(data_source=)) — review that first. This PR's diff is just the guard bypass on top of it.

The problem

add_table() refuses to run once _server_initialized is True — a guard meant to stop you from reconfiguring tables after a conversation is already underway. But server(data_source=) (added in #301) reuses that same public add_table(), and _server_initialized gets set to True the very first time .server() runs.

Since every new Shiny session re-runs the server function, this means server(data_source=) only ever works for the first session. A second browser tab, a page refresh, or simply a second concurrent user hits:

RuntimeError: Cannot add tables after server initialization.

(This is the same bug R's existing $server(data_source = ) has today — it isn't Python-specific.)

The fix

add_table()'s body (name validation, staging the table, rebuilding the system prompt/executor cache, greeting inclusion) is extracted into a guard-free _add_or_replace_table(). add_table() keeps its guard and calls into that core; .server(data_source=...) calls the core directly, bypassing the guard for this path only.

The public add_table()/add_tables() guard is unchanged — you still can't call those after .server() has run.

Test plan

  • New test: two sequential .server(data_source=...) calls (simulating two sessions) both succeed.
  • New test: add_table() still raises after .server() has run, confirming the public guard is untouched.
  • Existing guard test in test_multi_table.py still passes.

add_table()'s "no changes after server initialization" guard exists to
protect prompt-cache coherence once a conversation is underway, but it
also blocks server(data_source=...) on every session after the first --
since every new session re-runs the server function, this made
per-session registration usable for exactly one session.

Extract add_table()'s guard-free core into _add_or_replace_table() and
call it directly from .server(), bypassing the guard for this path
only; the public add_table()/add_tables() guard is unchanged.

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.

🟡 Changes recommended

Session-owned resources and per-session greeting snapshots must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes repeated Shiny server(data_source=...) sessions while preserving the public table-registration guard.

Changes:

  • Extracts guard-free table registration logic.
  • Adds regression tests for repeated sessions and guard preservation.
  • Documents the behavior in the changelog.
File summaries
File Summary
pkg-py/tests/test_server_data_source.py Adds repeated-session and guard-preservation tests.
pkg-py/src/querychat/_shiny.py Uses deferred registration; critical (2 votes) session resource ownership and moderate (3 votes) greeting snapshot issues remain.
pkg-py/src/querychat/_querychat_base.py Adds shared registration logic; critical (1 vote) resource cleanup issue and nit (1 vote) naming issue remain.
pkg-py/CHANGELOG.md Documents the session behavior.
Review details

Suppressed comments (1)

pkg-py/src/querychat/_querychat_base.py:501

  • This new method has a leading underscore even though _querychat_base.py is already a private module. The repository guideline for private modules requires regular names without _ prefixes; rename it to add_or_replace_table and update the caller so this cross-module helper is not presented as a hidden API.
    def _add_or_replace_table(
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg-py/src/querychat/_querychat_base.py
Comment thread pkg-py/src/querychat/_shiny.py
Comment thread pkg-py/src/querychat/_shiny.py

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.

🟢 Approval recommended

Only minor nits remain; no blocking issues were identified.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Lite

include_in_greeting=include_in_greeting,
)

def _add_or_replace_table(

qc.server(data_source=other_users_df) # must not raise

assert list(captured_mod_server[1]["data_sources"].keys()) == ["users"]
cpsievert added a commit that referenced this pull request Sep 12, 2026
$add_table() (which $server(data_source=) calls internally) refuses to
run once .server_initialized is TRUE -- a guard meant to stop
reconfiguring tables after a conversation is underway, but which also
blocked a second Shiny session's own $server(data_source=) call. Even
bypassing that, replacing a table cleaned up the resource (and cached
query executor) an earlier, still-running session depended on, and the
greeting_arg closure read live private$.data_sources/greeter$tables at
async-invocation time, so a later session's registration could leak
into an earlier session's greeting.

Extracts $add_table()'s guard-free core into a private
add_or_replace_table(), used directly by $server()'s data_source= path
with cleanup_replaced = FALSE so it no longer tears down an
earlier session's resource. $server() now also snapshots
greeter$tables and passes it (plus the already-per-session
data_sources) through mod_server() to greeter$build_client(), so the
lazily-invoked greeting reads a point-in-time snapshot instead of live,
mutable state.

Mirrors the equivalent Python fixes (#302, #303,
cpsievert added a commit that referenced this pull request Sep 12, 2026
$add_table() (which $server(data_source=) calls internally) refuses to
run once .server_initialized is TRUE -- a guard meant to stop
reconfiguring tables after a conversation is underway, but which also
blocked a second Shiny session's own $server(data_source=) call. Even
bypassing that, replacing a table cleaned up the resource (and cached
query executor) an earlier, still-running session depended on, and the
greeting_arg closure read live private$.data_sources/greeter$tables at
async-invocation time, so a later session's registration could leak
into an earlier session's greeting.

Extracts $add_table()'s guard-free core into a private
add_or_replace_table(), used directly by $server()'s data_source= path
with cleanup_replaced = FALSE so it no longer tears down an
earlier session's resource. $server() now also snapshots
greeter$tables and passes it (plus the already-per-session
data_sources) through mod_server() to greeter$build_client(), so the
lazily-invoked greeting reads a point-in-time snapshot instead of live,
mutable state.

Mirrors the equivalent Python fixes (#302, #303,
cpsievert added a commit that referenced this pull request Sep 12, 2026
$add_table() (which $server(data_source=) calls internally) refuses to
run once .server_initialized is TRUE -- a guard meant to stop
reconfiguring tables after a conversation is underway, but which also
blocked a second Shiny session's own $server(data_source=) call. Even
bypassing that, replacing a table cleaned up the resource (and cached
query executor) an earlier, still-running session depended on, and the
greeting_arg closure read live private$.data_sources/greeter$tables at
async-invocation time, so a later session's registration could leak
into an earlier session's greeting.

Extracts $add_table()'s guard-free core into a private
add_or_replace_table(), used directly by $server()'s data_source= path
with cleanup_replaced = FALSE so it no longer tears down an
earlier session's resource. $server() now also snapshots
greeter$tables and passes it (plus the already-per-session
data_sources) through mod_server() to greeter$build_client(), so the
lazily-invoked greeting reads a point-in-time snapshot instead of live,
mutable state.

Mirrors the equivalent Python fixes (#302, #303,
cpsievert added a commit that referenced this pull request Sep 12, 2026
$add_table() (which $server(data_source=) calls internally) refuses to
run once .server_initialized is TRUE -- a guard meant to stop
reconfiguring tables after a conversation is underway, but which also
blocked a second Shiny session's own $server(data_source=) call. Even
bypassing that, replacing a table cleaned up the resource (and cached
query executor) an earlier, still-running session depended on, and the
greeting_arg closure read live private$.data_sources/greeter$tables at
async-invocation time, so a later session's registration could leak
into an earlier session's greeting.

Extracts $add_table()'s guard-free core into a private
add_or_replace_table(), used directly by $server()'s data_source= path
with cleanup_replaced = FALSE so it no longer tears down an
earlier session's resource. $server() now also snapshots
greeter$tables and passes it (plus the already-per-session
data_sources) through mod_server() to greeter$build_client(), so the
lazily-invoked greeting reads a point-in-time snapshot instead of live,
mutable state.

Mirrors the equivalent Python fixes (#302, #303,
@cpsievert
cpsievert added this pull request to stack #307 September 12, 2026 16:09
@cpsievert
cpsievert merged commit 0281676 into main Sep 12, 2026
9 checks passed
@cpsievert
cpsievert deleted the fix/py-server-guard-bypass branch September 12, 2026 16:10
cpsievert added a commit that referenced this pull request Sep 12, 2026
$add_table() (which $server(data_source=) calls internally) refuses to
run once .server_initialized is TRUE -- a guard meant to stop
reconfiguring tables after a conversation is underway, but which also
blocked a second Shiny session's own $server(data_source=) call. Even
bypassing that, replacing a table cleaned up the resource (and cached
query executor) an earlier, still-running session depended on, and the
greeting_arg closure read live private$.data_sources/greeter$tables at
async-invocation time, so a later session's registration could leak
into an earlier session's greeting.

Extracts $add_table()'s guard-free core into a private
add_or_replace_table(), used directly by $server()'s data_source= path
with cleanup_replaced = FALSE so it no longer tears down an
earlier session's resource. $server() now also snapshots
greeter$tables and passes it (plus the already-per-session
data_sources) through mod_server() to greeter$build_client(), so the
lazily-invoked greeting reads a point-in-time snapshot instead of live,
mutable state.

Mirrors the equivalent Python fixes (#302, #303,
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.

2 participants