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.

Resolved design. Isolate individual command exceptions within the receive loop and return sanitized 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 pointCurrent behaviorBoundary
server._websocket_handlerThe 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_commandUnknown 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_upWait 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/deleteCancellation 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

1
Receive text; log sanitized diagnostics for invalid JSON and continue receiving.
2
After parsing an object, execute the action handler. Unknown actions return operation_error, echoing a valid request_id.
3
Log handler exceptions structurally with only action/session/type; return code=handler_error to the same socket, then continue receiving the next command.
4
After transport disconnection, first remove the current socket from the connection set, then check whether another connection still focuses its session.
5
If no other focused connection remains and a legacy follow-up queue exists, enqueue the unique sentinel. The finally block in _web_follow_up still deletes the queue.
6
The waiter converts the sentinel to None; a real empty string remains an empty string, so they are distinguishable.

04 · Trust boundaries, failures, and compatibility

05 · Explicit exclusions

06 · Acceptance matrix

Public entry pointScenarioObservable assertion
_websocket_handlerThe first registered action raises KeyError; the second is pingThe same connection first receives sanitized operation_error, then pong; the first command did not close the connection.
_websocket_handlerInvalid JSONIgnore the frame and process the next one without echoing the original text.
_web_follow_up + disconnectThe only focused connection disconnectsThe waiting thread returns None within the test deadline rather than waiting 300 seconds; the queue is eventually deleted.
disconnectAnother connection still focuses the same sessionDo not enqueue a sentinel; keep waiting. Return the ordinary answer submitted by the other connection.
follow_up_answerThe user submits an empty stringReturn ""; do not interpret it as a disconnection.

07 · Implementation files and evidence

FileResponsibility
apps/server/openprogram_server/server.pyCommand-level exception boundaries, sanitized error frames, connection-observer cleanup, and follow-up sentinel interpretation.
tests/component/webui/runtime/test_websocket_command_lifecycle.pyPublic-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.