OpenProgram / WebUI reliability / implementation handoff
WebSocket command and interactive-wait lifecycle
This page defines ownership and observable behavior for individual WebSocket command failures, disconnections, and legacy ask_user waits. See Section 07 for implementation status and verification evidence.
operation_error; the new frontend continues to consume the old server’s action_error. The dispatcher does not clean up reservations acquired by actions, avoiding deletion of another connection’s ownership. On disconnection, send an in-process sentinel to the legacy follow-up queue only if no other connection focuses that session. The waiter returns None upon receiving the sentinel; disconnection must not be interpreted as an empty-string answer.01 · Current implementation and boundaries
| Entry point | Current behavior | Boundary |
|---|---|---|
server._websocket_handler | The receive loop separately isolates JSON parsing and individual handler exceptions. A handler failure returns operation_error and then continues receiving subsequent commands. | Transport disconnection and failure to send an error frame still terminate the connection. The dispatcher does not infer ownership of resources acquired by actions. |
server._handle_ws_command | Unknown actions return operation_error(code=unknown_action); exceptions from registered handlers propagate to the receive loop, where the same constructor returns handler_error. | The wire response contains only constrained action, session, and request metadata and registered error categories. Exception details go only to local logs. |
server._web_follow_up | Wait using a session queue; when the last observer disconnects or cancellation occurs, return None; a real empty string remains "". | The legacy queue remains process-local state and does not provide durable recovery across worker restarts. |
| stop/delete | Cancellation objects and disconnection sentinels are both treated as non-answers. The owner’s finally block cleans up the queue. | The generic dispatcher does not clean up reservations by session. The action that acquired a resource must roll back using its own owner token. |
02 · Reference mechanisms and trade-offs
Starlette WebSocket
Transport disconnection uses WebSocketDisconnect to terminate the connection. Place connection-level cleanup in finally and handle command-level application exceptions within the receive loop.
QuestionRegistry
New runtime questions already use a session-scoped, claim-once cancel_session(). Legacy follow-up does not migrate its protocol; it adopts only the same “cancellation is not an answer” semantics.
Existing run reservation
_release_run_reservation(session_id,msg_id) already performs owner-scoped cleanup. Because the dispatcher lacks an owner token, generic exception handling must not clean up by session.
Adopt: command-level error frames, a sentinel when the last observer disconnects, and owner-scoped action cleanup.Modify: the legacy queue return type to allow None.Reject: cancellation whenever any tab disconnects, dispatcher deletion of reservations by session, closing the connection after an application exception, and passing sentinels to business functions.
03 · Target data flow
operation_error, echoing a valid request_id.code=handler_error to the same socket, then continue receiving the next command._web_follow_up still deletes the queue.None; a real empty string remains an empty string, so they are distinguishable.04 · Trust boundaries, failures, and compatibility
- Client payloads are untrusted. Error frames must not echo payloads, exception messages, paths, credentials, or tracebacks.
- Logs may record action, session_id, and exception type; tracebacks go only to the local logger.
- After acquiring a reservation or another resource, a handler must roll back using its own owner token. The dispatcher isolates transport handling and does not infer business ownership.
WebSocketDisconnectand failure to send an error frame still terminate the connection. Socket writes must not retry indefinitely.- Multi-window compatibility: closing one tab does not cancel the question while another connection still focuses the same session.
- legacy
follow_up_answerwire shape remains unchanged; ordinary string answers and real empty strings remain valid.
05 · Explicit exclusions
- Do not refactor the legacy global
set_ask_userinto QuestionRegistry. - Do not change runtime.ask, ACP, channel questions, the send queue, or the run reservation protocol.
- Do not address Windows, system Keychain/keyring, orphan files, or other audit items.
06 · Acceptance matrix
| Public entry point | Scenario | Observable assertion |
|---|---|---|
_websocket_handler | The first registered action raises KeyError; the second is ping | The same connection first receives sanitized operation_error, then pong; the first command did not close the connection. |
_websocket_handler | Invalid JSON | Ignore the frame and process the next one without echoing the original text. |
_web_follow_up + disconnect | The only focused connection disconnects | The waiting thread returns None within the test deadline rather than waiting 300 seconds; the queue is eventually deleted. |
| disconnect | Another connection still focuses the same session | Do not enqueue a sentinel; keep waiting. Return the ordinary answer submitted by the other connection. |
follow_up_answer | The user submits an empty string | Return ""; do not interpret it as a disconnection. |
07 · Implementation files and evidence
| File | Responsibility |
|---|---|
apps/server/openprogram_server/server.py | Command-level exception boundaries, sanitized error frames, connection-observer cleanup, and follow-up sentinel interpretation. |
tests/component/webui/runtime/test_websocket_command_lifecycle.py | Public-entry regression tests for the actual receive loop, disconnection, and waiting threads. |
The first implementation slice is complete and has passed independent specification and quality reviews. command-level isolation, sanitized operation_error, frontend compatibility with old frames, and follow-up disconnection semantics are covered by public-entry regressions. Persistent error storage, acknowledgement, and reconnect recovery are outside this page’s implementation scope.
Interaction responsiveness
After a successful socket write, the sender immediately clears only the captured composer draft and renders a locally pending user message in its owning conversation. The pending reservation retains the exact text and attachments until authoritative acknowledgement. The server acknowledgement reconciles the local identity without duplicating messages; rejection preserves a recoverable message without overwriting a newer draft. A pending client message is not proof that model execution started.
Unrelated resource projection and session/sidebar work must not delay message acceptance or block the server event loop. A resource close targets the exact page and association identities, preserves generation and sequence ordering, and does not scan unrelated execution inputs. Local close feedback remains separate from native close confirmation; failure or a newer resource generation cannot be hidden by an old pending action.
Controlled tests cover delayed acknowledgement, rejection, session switching, bound composers, queue/steer behavior, resource close ordering, and unrelated event-loop progress. Production latency is not inferred from synthetic timings. This work changes source only: the installed App and running worker are not rebuilt, packaged, restarted or operated.
Implementation status: source implementation passes independent specification and quality review. Public-entry regression checks cover socket acceptance, acknowledgement reconciliation, recoverable rejection, session-history concurrency, attachment cancellation, and confirmed native closure. Runtime installation is deferred. Backend changes are limited to the identified blocking call paths; no general-purpose protocol replacement or concurrency framework is introduced.