Skip to content

Add persistent session support#28

Merged
jackcbrown89 merged 7 commits into
mainfrom
worktree-jb-persistent-session-support
Apr 17, 2026
Merged

Add persistent session support#28
jackcbrown89 merged 7 commits into
mainfrom
worktree-jb-persistent-session-support

Conversation

@jackcbrown89
Copy link
Copy Markdown
Contributor

@jackcbrown89 jackcbrown89 commented Apr 17, 2026

Summary

  • New end_session_message in the wire protocol; Node session now runs N sequential invocation_message / command_execution_message exchanges over one WebSocket, with per-request AbortController / ArtifactManager so cancel_message aborts only the in-flight request.
  • Python client gains RuntimeUseSession and RuntimeUseClient.session() async-context-manager for persistent sessions; existing one-shot query() / execute_commands() kept as convenience wrappers over the same transport.
  • Bumps runtimeuse and runtimeuse-client to 0.11.0.

Test plan

  • npm test in packages/runtimeuse (136 passing, incl. new "two sequential command_execution_messages on one socket" case)
  • pytest in packages/runtimeuse-client-python (43 passing, incl. new TestPersistentSession cases: two sequential calls, mixed query+commands, cancel mid-session + follow-up, per-call abort, close-on-exit)
  • Manual smoke: open a session against a real runtime, run two queries over one WS

Note

Medium Risk
Introduces a new persistent request/response mode over a single WebSocket and updates cancellation/terminal-message behavior, which could affect protocol compatibility and edge cases around abort/cleanup.

Overview
Adds persistent session support across the runtime and Python client: a new end_session_message allows multiple sequential invocation_message / command_execution_message exchanges over one WebSocket without reconnecting.

On the Node runtime, WebSocketSession now treats each invocation/command as an isolated request with its own AbortController, supports cancel_message to abort only the in-flight request (without closing the socket), and keeps a session-scoped ArtifactManager that can watch multiple artifacts_dir values and defers watcher draining/finalization to session close.

On the Python client, introduces RuntimeUseClient.session() / RuntimeUseSession built on new persistent transport interfaces (PersistentTransport, ConnectedTransport) and refactors message handling into a shared request loop that drains server terminals after abort to prevent stale errors leaking into subsequent calls. Versions are bumped to 0.11.0 and tests/examples are added/updated accordingly.

Reviewed by Cursor Bugbot for commit d49018f. Bugbot is set up for automated code reviews on this repo. Configure here.

Lets a client run N sequential invocation/command_execution requests over a
single WebSocket. Adds end_session_message (client -> server) and reworks the
Node session around per-request AbortController/ArtifactManager so cancel
aborts only the in-flight request.

Python: new RuntimeUseSession + RuntimeUseClient.session() context manager;
one-shot query()/execute_commands() preserved as convenience wrappers.

Bumps runtimeuse and runtimeuse-client to 0.11.0.
Comment thread packages/runtimeuse/src/invocation-runner.ts
Comment thread packages/runtimeuse-client-python/src/runtimeuse_client/client.py
Comment thread packages/runtimeuse/src/session.test.ts
Post-agent command failures previously threw out of run(), causing the
session catch block to replace the successful agent result with an
error_message — the client would never see the actual result.

Now post-agent failures emit an error_message (so the client is notified)
but the agent's result_message is still returned as the terminal.
Comment thread packages/runtimeuse/src/invocation-runner.ts Outdated
runCommands emitted error_message on non-zero exit and also threw, so pre-agent
failures produced one frame and post-agent failures produced two (both from
runCommands and from the subsequent catch block I added). Either way, the
error_message is terminal on the wire — the Python client raises on it and
never consumes result_message.

Now runCommands only throws; the session's outer catch emits a single
error_message for pre-agent failures, and post-agent failures are logged
server-side so result_message remains the sole terminal.
Comment thread packages/runtimeuse/src/session.ts Outdated
Comment thread packages/runtimeuse/src/session.ts Outdated
Wrap handleRequest's body in try/finally so requestInFlight, the abort
controller, and the artifact manager are always cleared. Previously, if
stopWatching / waitForPendingRequests / waitForAll threw (or were cancelled),
requestInFlight stayed true and every subsequent request was rejected with
"another is in flight", defeating the whole point of a persistent session.

Also swallow drain errors (log only) so a watcher failure can't abort the
terminal send.
Comment thread packages/runtimeuse/src/session.ts
Preserve the pre-refactor behavior: a failed post-agent command terminates
the request with error_message, so the Python client raises AgentRuntimeError.
runCommands still doesn't emit error_message itself — the session's outer
catch is the single wire emit point, so there's no double-emit.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 55fd083. Configure here.

Comment thread packages/runtimeuse-client-python/src/runtimeuse_client/client.py
Comment thread packages/runtimeuse/src/session.ts Outdated
Previously the watcher was torn down and recreated per request, which
forced each request to block on a 3s chokidar-drain window before the
terminal could be sent — so sequential calls on a persistent session were
spaced by the delay.

Now ArtifactManager lives for the whole session and exposes addDirectory()
so each request just registers its artifacts_dir with the existing watcher.
The drain (3s delay + stopWatching + waitForPendingRequests + waitForAll)
happens once, on session close, catching files written right before the
ws closed. Requests return as soon as the agent (and post-agent commands)
finish.

Also tightens mocks in session.test.ts (mockReset in beforeEach) and
covers the new behavior with "stops watcher on close, not per request"
and "registers each request's artifacts_dir on the shared watcher".

Additionally pulls in prior fixes in this branch:
- _run_request_loop drains the server's terminal before raising
  CancelledException, preventing stale error_messages from leaking into
  the next request on a persistent session.
- command-handler coerces non-numeric error.code (e.g. ABORT_ERR) to -1.
- session overrides terminal with "Request cancelled" when the runner
  returned a partial result after abort.
@jackcbrown89 jackcbrown89 merged commit 656772e into main Apr 17, 2026
6 checks passed
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