Do Not Bind Tasks to Windows: How Open Nua Rebuilt Its Desktop Agent Session Architecture
Open Nua Engineering
What should happen to a desktop Agent task when the user closes the window, briefly loses the network, or opens the same conversation from another device?
It sounds like a reconnection problem. In practice, it raises more fundamental questions: Who owns session state? Which answer wins if two clients respond to the same confirmation? Can tool calls, TODOs, subagents, Artifacts, and token usage be restored after the live stream ends? Can one slow client stall the Agent itself?
Open Nua Desktop eventually stopped patching the old “window connected to execution” model and redrew the boundary between execution, sessions, and clients:
Agent execution belongs to the session, not to a window. A window is only one observer and operator of that session.
This article does not depend on a particular protocol. It covers a system-design problem shared by any long-running Agent product with multiple clients.
The Real Problem Is Fact Ownership, Not Reconnection
An early desktop Agent often starts as a direct chain: the Renderer invokes a request, the main process hands it to the Agent runtime, and the runtime streams text and tool events back while the Renderer updates the page.
That works for short conversations. Once the product adds long-running work, tools, human confirmation, subagents, automation, and remote assistants, the Renderer starts doing work that does not belong to UI: deciding whether a turn is still running, assembling tool state, buffering streams, reconstructing the page after refresh, and interpreting slightly different events from every entry point.
One task then acquires several competing “facts”: the runtime has a checkpoint, the main process has an execution journal, the Renderer has live messages, the remote assistant has progress cards, and the automation scheduler has another run state.
They appear consistent while everything stays online. Disconnects, restarts, duplicate events, and concurrent operations expose the cracks:
- The Agent has finished, but the page still says it is thinking.
- Tool calls appear live and disappear after reopening.
- A subagent keeps its task card but loses child messages and its result.
- One client has answered a confirmation while another can still submit it.
- Final answers, tool results, and subagent cards change order.
- The token display reports only the last model call.
- A slow client blocks execution.
These are not seven unrelated UI bugs. They are one architectural problem: the system lacks a single, recoverable session fact source that every client can observe.
Separate Runtime, Session Host, and Client
The new architecture introduces an independent role: the Session Host.
Each layer owns one kind of fact.
The Agent Runtime owns execution facts. It runs model calls, tools, checkpoints, context, subagents, and resumed execution. It does not need to know whether a button is visible, and it must not end a task merely because the Renderer disconnects.
The Session Host owns client-visible facts. It projects runtime events into stable Session, Turn, Message, Tool Call, Input Request, Artifact, and Worker state. It maintains ordered actions, snapshots, subscriptions, and conflict handling.
A Client observes state and expresses intent. Desktop, CLI, remote assistants, and Automation can subscribe to the same session and submit messages, cancellation, confirmation, or additional input. No client decides session facts on its own.
With that boundary in place, the Agent can continue when every client disconnects. A reconnecting client does not need every in-memory event it missed; it can replay actions after a known sequence or request a fresh authoritative snapshot.
A Session Is Mergeable State, Not a Text Stream
A Turn contains more than user and assistant messages. It may include reasoning, tool lifecycles, HITL, TODOs, subagents, user-selected files or images, Artifacts, file changes, usage, and traces.
Appending whatever arrives to the page is not enough. The Session Host applies ordered actions to pure state, while clients share the same reducer:
previous state + ordered action = next state
Actions may be delivered more than once, but they need stable identities and ordering. If a client updates optimistically and the Host rejects the operation, it must roll back or replace its view with authoritative state. Multi-client consistency no longer depends on everyone happening to receive messages in the same order.
This Rebuild Finally Made Me Understand Redux
I used to think of Redux as a frontend technique for putting global variables in one place. Seen that way, it looks unnecessarily heavy: changing one field requires an action, a dispatch, and a reducer, while local state would often be simpler.
Only when live events, history recovery, multiple clients, and optimistic updates all had to converge on one session did its real value become clear. Redux is not primarily about where variables live. It constrains how the system is allowed to change state.
| Redux concept | Equivalent in the session architecture |
|---|---|
| Store | The session projection currently held by a client |
| Action | An identified, ordered state change published by the Host |
| Reducer | The convergence rule that deterministically derives new state |
| Replay / DevTools | Reconstructing a situation from action history or a snapshot |
When every change passes through a describable action and a pure reducer, the same inputs produce the same state. Duplicate events can be deduplicated, invalid ordering can be detected, disconnected clients can replay, and state evolution can be tested without launching the entire UI. Redux trades ceremony for traceability, replayability, and verification.
The Session Host is not simply a larger Redux Store. Redux provides deterministic projection; a distributed session must also decide authoritative ordering, whether an action is accepted, and how concurrent conflicts are resolved. Those belong to the Host. The client reducer ensures that every participant derives the same result from the same confirmed facts.
History recovery is where this distinction is easiest to miss. A capability that works live is not necessarily a reliable session capability. A live subagent may come from worker events while history restores only the main conversation text. An Artifact may be structured during streaming and collapse into ordinary tool output after checkpoint reconstruction.
Every user-visible state therefore has to complete four stages:
- Produce: the Runtime emits a real state change.
- Converge: the Session Host and reducer apply it.
- Recover: a snapshot or history reconstructs it.
- Render: a reconnected client presents it and remains interactive.
A feature that completes only the first two stages is still a live-broadcast feature. History can no longer be an assistant-text list; it must retain enough structure to rebuild turn identity, tool pairing, subagent relationships, input requests, Artifact references, usage, and required metadata.
HITL Is a Concurrency Protocol, Not a Form
The same confirmation request may appear on Desktop and in a remote assistant. Both clients may answer. The system must define whether the request is still valid, which response wins, what the later submitter receives, and whether reconnecting creates another logical request.
Open Nua treats an Input Request as a Session Host resource and uses first-writer-wins. A client submits an action to complete the request instead of calling runtime resume directly. The Host verifies that the request, turn, and tool call still match before delivering one valid answer to the runtime:
invoke → interrupt → publish request → accept one answer
→ resume → record tool result → terminal state
The whole lifecycle must be tested. Proving that a button calls resume says nothing about duplicate submissions, reconnection, or safe tool side effects.
External Side Effects Need a Clear Acceptance Point
When an Agent sends a message or updates a card through a remote assistant, two facts exist: whether the Host accepted the capability call, and whether the external platform completed delivery. If they are mixed together, a network timeout leaves the Agent unsure whether to retry, while duplicate events may produce duplicate messages.
The new implementation deduplicates by stable interrupt and tool identities and caches both the in-flight promise and final result. Once the Host accepts the call, reconnecting clients do not execute it again. External delivery failures enter a separate delivery state and history, where the delivery layer decides whether to retry or expose the error.
This gives each side effect a clear commit point instead of assuming that an asynchronous function will probably run only once.
Backpressure Protects Connections, Not the Agent
Backpressure still matters, but it should protect connection memory. It must not turn a client's consumption speed into the Agent's execution speed.
The Session Host keeps bounded send and replay state per client. A lagging client can abandon incremental catch-up and request a snapshot. When it disconnects completely, the Host releases connection resources while the runtime continues. Large tool outputs and Artifacts are referenced and fetched on demand rather than rebroadcast in full on every update.
The important decision is not a queue length. Client state may be discarded and rebuilt; Agent execution state may not.
Usage and Traces Describe the Whole Turn
A complex task often performs several model calls: main-Agent planning, reasoning after tools, context processing, and one or more subagents. Treating the final assistant message's usage as Turn usage systematically undercounts cost and distorts cache-hit ratios.
Usage is therefore aggregated across every model call in the Turn, retaining input, output, cache read and creation, reasoning, and model identity, with subagent usage attributed to the parent Turn.
Traces follow the same principle. A subagent is not an unrelated trace tree; it is a child execution under the parent Turn. session_id, turn_id, run_id, parent_run_id, client_id, and action sequence must travel through the pipeline from the start. They cannot be reconstructed reliably from log text after a failure.
Migration Is Not Complete Until the Old Fact Sources Are Deleted
The most dangerous migration strategy is to keep old and new projections indefinitely and bridge them at the edges. It lowers short-term switching risk, but every later bug gains another question: “Which path produced the state on this page?”
We set a clear cutover boundary: preserve the Runtime's Agent loop, checkpoints, and tools; use an adapter to emit stable session actions; move Desktop, CLI, Automation, and the remote assistant onto the same session; then delete the old services, normalizers, projection store, and duplicate recovery paths. The UI is not allowed to fall back silently.
The CLI also shares the Renderer's types, reducer, and connection semantics. It is tested as a real subprocess with stdout, stderr, exit codes, and a Fake Host. That makes it more than a diagnostic interface: it can prove that one client may start a task and exit while another later observes the correct result.
Replace “It Worked” with a Protocol Conformance Matrix
We call the cross-client test table a protocol conformance matrix:
| Scenario | Desktop | CLI | Remote assistant | Automation | Core assertion |
|---|---|---|---|---|---|
| Tools and streaming | Display | Diagnose | Incremental update | Observe | Ordered, no duplicates |
| HITL | Answer | Answer | Answer | Recover | First-writer-wins |
| Disconnect and reconnect | Recover | Exit safely | Go offline | Unaffected | Agent keeps running |
| TODOs / subagents | Full display | Inspect | Display as needed | Observe | State and relationships preserved |
| Artifact | Open | Locate | Send reference | Locate | Resolves after restart |
| Usage / trace | Whole Turn | Inspect | Not required | Audit | Includes subagents |
| Slow or concurrent clients | Converge | Converge | Converge | Converge | Bounded, explicit conflicts |
The most important test is not “send a message and receive a reply.” Start a long task containing tools, HITL, a subagent, and an Artifact. Disconnect the client completely. Let the task continue. Connect elsewhere and answer HITL. Restart again. Finally, verify that message order, TODOs, subagents, Artifacts, whole-Turn usage, and trace relationships are still complete.
Conclusion: Tasks Belong to Recoverable Sessions
This rebuild left us with a few principles we will not compromise again:
- Protocol migration begins with migrating fact ownership.
- Every state must be tested across live, disconnect, restart, and history paths.
- Side effects require lifecycle verification from interrupt to terminal state.
- Parent-child runs, whole-Turn usage, and action ordering need observability from day one.
- Every client must pass the same protocol conformance matrix.
- A migration is incomplete until old fact and recovery paths are removed.
Every long-running Agent product eventually faces the same question: when every interface temporarily leaves, who does the task belong to?
Open Nua's answer is the recoverable session. Windows may close, connections may be rebuilt, and clients may change. Execution facts, user input, and final results should remain continuous.