Repository navigation
Server push (spec 8): actions declare what they change, the server tells every GUI - #990
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This was referenced Oct 16, 2026
…s the GUI (spec 8) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with its implementation plan The examples the spec cites are bookmarks, bank and lims; ledger's derived report becomes bank's SpendingReport. Matching runs on the first registry read, since a release build's latch closes only on request. The explorer shows effects and records. The MCP adapter's shape is fixed. Delivery is one PR in nine ordered parts, planned in docs/superpowers/plans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
std::ranges::iota is missing from Apple's libc++ as it is from Emscripten's, so the table suite did not compile on macOS. The library already uses std::iota for the same reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ypes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r one id is a conflict Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ffect validation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from a slow reader Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ect handler SocketServer hands RemoteServer a ConnectionWriter for each accepted connection. Every frame handed to it, replies and events alike, is counted on the connection's atomic backlog by the size of its encoded envelope, and released on the loop once its last chunk is written or it is dropped; a release that takes the backlog below kEventBacklogLow calls connectionDrained. SocketBackend routes event envelopes before reply matching and posts them to the setEventHandler executor, and runs the setConnectHandler handler after each connect, the first included. RemoteServer keeps an owed resync when a connection's writer is replaced, and catches a throwing backlog() read like a throwing write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nts in their own header SocketServer hands RemoteServer a connection's writer only once the 101 response is queued, so an event posted to a connection still in its handshake is dropped instead of reaching the peer ahead of the 101. ConnectionWriter and the backlog bounds move to core/connection_writer.hpp, so the shared connection code no longer compiles RemoteServer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QtWebSocketServer hands RemoteServer a ConnectionWriter for each connection as it is accepted (the upgrade is already complete by then). Every frame written to a connection, replies and events alike, counts toward its atomic backlog by the size of its encoded envelope; a FIFO of each frame's end position in the socket's written-byte count releases it from bytesWritten, and a release that crosses below kEventBacklogLow calls connectionDrained. QtWebSocketServerConfig::sendTimeout (default 30 s, as net) aborts a peer whose oldest unwritten frame has outlived it. QtWebSocketBackend routes event envelopes before reply matching and posts them to the setEventHandler executor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Over wss:// QWebSocket::bytesToWrite() counts only plaintext not yet encrypted, which Qt encrypts eagerly, so the server released a frame long before its ciphertext was sent: a stalled TLS reader got every event and never tripped sendTimeout. A frame's release position is now measured when it is handed over -- bytes written so far plus everything the socket stack holds, the QSslSocket's encryptedBytesToWrite() included -- in a small detail::OutboundLedger, and "nothing left to write" means nothing in either buffer. The QSslSocket is found among the QWebSocketServer's descendants by the connection's endpoints. Tests cover a stalled reader held back and resynced over ws and wss, the send timeout over wss, and the ledger's release by position; the cleared-handler check no longer passes vacuously. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…every transport Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tionFacts IEnvelopeService gains a non-pure three-parameter handle taking the connection's ConnectionFacts, whose default forwards to the pure two-parameter one. RemoteServer passes the facts of the envelope's cid, kept beside the connection scopes on the server strand and dropped by closeConnection; a message with no open connection gets throwaway facts. eventsNegotiated(cid) reads the flag, server strand only. HelloRequest and HelloReply gain an additive `events`. UiService accepts an offer on a hello it answers ok and sets facts.events; the generic client's ui-hello (clientHello) offers it. The in-repo two-parameter services add `using IEnvelopeService::handle;`: GCC's -Woverloaded-virtual at level 2 flags the hidden overload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reply-effect and change-event functions are split into one method per kind of change, around a Marks collector and helpers for a record's rows at its paths, which brings each under the complexity limit. markStale is static. Qt's queued callable is owned by Qt, which the analyzer cannot see. The test leftovers reserve their vectors and drop an unused using. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…embers Its member fold is empty then, so GCC reports the helper lambda as set but not used, which -Werror makes an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
A query whose action declares effects is refused at load: a query must read. An evaluate query is exempt, since an evaluation runs no handler. Three warnings, collected in Document::warnings and logged by the bundle registry and the screen registry, never refuse a document: a refresh declaration (invalidates, patch, refreshOn) the mutation's effects already cover; a mutation whose action declares nothing and that refreshes nothing; and a watch of a query, from any source, that runs a mutation with effects, which can make two clients refetch each other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
The drain finds nothing in flight as soon as the server has answered what it has read, which under UBSan was before it had read all 128 requests: the event then went out ahead of the replies. The test now waits until the model has run every execute, then for the drain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…turns Its Facts gain an effects row (each effect, or reads when the action declares nothing) and, when the result holds records, a records row with each record's key field and paths, so why a screen did or did not update can be read without the source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
One RemoteServer and SocketServer, two SocketBackend clients, each with a UI session wired to the server's events and watches, mounting one screen whose query declares no refresh. Client A renames a row; A updates from its reply's effects and B from the server's event. With B's events dropped, B's check fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…esh declarations Bookmarks_Bookmark, Bookmarks_BookmarkDetail and Bookmarks_Tag are records with private keys, and every action declares what it changes. The screens lose their invalidates and their refetch-only onSuccess commands. A shared testkit helper loads every document of a served bundle against its catalog facts, and each rung asserts it loads without refusals or warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…eclarations Accounts, transactions, cards, payees, loans, payments, budgets and notifications are records with private keys, declared identically for both model sets; Bank_SpendingReport is derived from transactions. The screens lose their invalidates and refetch-only onSuccess commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…eclarations Results, samples, verifications, conflicts and analysis versions are lab-wide records. Lifecycle transitions update their sample, instance actions change their instance, and OpenSample changes nothing. The screens lose their invalidates, refreshOn, patch and refetch-only onSuccess commands; a re-capture, which replaces its row under a new key, now refetches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
McpAdapter speaks MCP's JSON-RPC 2.0 over one server connection: one tool per action the session may run, with the action's schema, a keyed model's tool taking its instance, and an evaluate tool for a dry run. A call is an ordinary execute, so every check applies and every watching GUI is told; a refused call is a tool result with isError. serveStdio drives it over MCP's stdio transport, pumping the adapter's owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
A record derived from one an action updates or changes changes as a whole too. The server expands derivations before an event leaves, but the caller's own reply applied only the declared effects, so in process, where no event comes back, a reply could not mark entries holding a derived record (read from the code; no rung screen shows one today). CatalogFacts::action now lists each derived record as changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
… documents and client The cache's row helpers take a record's paths and key field, so every optional is read where it is checked; sendWatch is const. The document checks move a query's into checkQuery, under the complexity limit. The shell registers the source's listeners in listen(). onConnected takes its listener by reference. The analyzer's leak report on Qt's queued callable is suppressed where its path starts, in our code. The effects link probe is listed as deliberately not profiled, beside the other link canaries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…et server The server installs a UiService over bank's screens, as the other rung servers do, and with --mcp-stdio runs an McpAdapter on stdin and stdout on a thread of its own: each permitted bank action is a tool, and a call reaches every watching GUI. Stdout then carries only JSON-RPC, so the status lines go to stderr; the server stops when stdin ends. The agent's identity is MORPH_MCP_PRINCIPAL, with MORPH_MCP_TOKEN as its token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…ll load
Under ui/2 a document refuses the three refresh keys, since actions
declare what they change; optimistic keeps its {query, at?, key, with}
shape as a definition of its own, and refreshEvery and refetch stay. A
ui/1 document loads exactly as before. The authoring layer emits ui/2
and no longer offers invalidates, patch or refreshOn; the generic client
speaks both vocabularies. A bundle needs the vocabularies its documents
declare. The goldens are regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
…perator[] - BRIDGE_REGISTER_RECORD and BRIDGE_REGISTER_EFFECTS suppress the unnamed namespace in a header once, at the macro, as BRIDGE_REGISTER_ACTION does. - McpAdapter reads members through findMember and builds replies from object initializers. That also makes a parse error's id null and evaluate's required list ["action","body"], which brace-initialising glaze's generic had turned into one-element arrays. - The adapter's destructor does not throw; handle() takes its message by const reference; serveStdio names its streams input and output. - Tests cover the keyed-model path (a tool's instance is the shared instance under that key) and each JSON-RPC fault. - Bank's server reads its arguments through a span and its environment before the agent thread starts; smaller fixes in the explorer, the document checks and the test kit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
Such a source never changes, so the record derived from it would never be refreshed, and nothing said so. Resolution now refuses it with the record's name, beside the cycle check. The specs list it among the refusals. Tests drive each branch of deriving changes and of resolution: arrays and wrappers before a path ends, merged keys and whole types, instances kept once, a record derived along two routes, updates of an unknown or keyless record, pathWithin, sameRegistration and a record held at two paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
… allowlist hints Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
There was a problem hiding this comment.
🟡 Changes recommended
Reconnect ordering, oversized-event handling, and the bank screens dependency leave unresolved correctness and build failures.
3 open issues
What changed in this PR
Adds action-driven server push so GUI caches refresh when another client or agent changes data, replacing per-screen refresh declarations.
Changes:
- Adds event delivery, connection backlog tracking, watches, and reconnect integration.
- Migrates authored screens to
ui/2with record and effect declarations. - Adds bank MCP serving, regression coverage, and supporting documentation.
| File | Description |
|---|---|
| tests/ui/golden/results.json | Updates vocabulary golden. |
| tests/ui/golden/palette.json | Updates vocabulary golden. |
| tests/ui/golden/declarations.json | Removes obsolete refresh declarations. |
| tests/ui/golden/commands.json | Updates vocabulary golden. |
| tests/ui/corpus/documents.json | Covers ui/2 validation. |
| tests/ui_interpreter_support.hpp | Adds effect and watch fixtures. |
| tests/test_ui_explorer.cpp | Tests effect display. |
| tests/test_ui_author.cpp | Migrates authoring tests. |
| tests/test_remote_envelope_service.cpp | Tests connection facts and compatibility. |
| tests/test_push_watch_table.cpp | Tests watch isolation and limits. |
| tests/table/test_table_view_change.cpp | Uses portable std::iota. |
| tests/table/test_table_sort.cpp | Uses portable std::iota. |
| tests/table/test_table_filter.cpp | Uses portable std::iota. |
| tests/push_records_late.hpp | Defines late-registration fixtures. |
| tests/push_records_late.cpp | Registers late fixture records. |
| tests/net/test_socket_backend.cpp | Exposes service overloads. |
| tests/net/CMakeLists.txt | Registers network push tests. |
| tests/compile_checks/effects_unseen_shared.hpp | Defines cross-unit fixtures. |
| tests/compile_checks/effects_unseen_b.cpp | Reads effects without registration visibility. |
| tests/compile_checks/effects_unseen_a.cpp | Checks shared effect registration. |
| tests/CMakeLists.txt | Registers push tests and probes. |
| src/qt/qt_websocket_backend.cpp | Routes events before replies. |
| scripts/check_coverage_objects.sh | Excludes the link probe. |
| scripts/branch_partial_allowlist.json | Updates coverage locations. |
| include/morph/ui/service.hpp | Negotiates events and bundle vocabularies. |
| include/morph/ui/service_client.hpp | Adds remote watcher construction. |
| include/morph/ui/frontend.hpp | Adds connection listeners. |
| include/morph/ui/explorer.hpp | Displays effects and records. |
| include/morph/ui/bundle.hpp | Adds event negotiation fields. |
| include/morph/qt/qt_websocket_server.hpp | Declares backlog and timeout support. |
| include/morph/qt/qt_websocket_backend.hpp | Declares event handlers. |
| include/morph/qt/detail/outbound_ledger.hpp | Tracks unwritten frames. |
| include/morph/net/socket_server.hpp | Installs connection writers. |
| include/morph/net/socket_backend.hpp | Adds connect and event callbacks. |
| include/morph/net/detail/ws_connection.hpp | Accounts for outbound backlog. |
| include/morph/core/observability.hpp | Adds oversized-event metric. |
| include/morph/core/detail/reply_router.hpp | Separates events from replies. |
| include/morph/core/detail/change_coalescer.hpp | Merges pending changes. |
| include/morph/core/connection_writer.hpp | Defines writers and backlog thresholds. |
| include/morph/core/change_types.hpp | Defines change payloads. |
| include/morph/core/backend.hpp | Adds backend event hooks. |
| examples/lims/screens/tests/test_lims_screens.cpp | Tests effect-driven screen updates. |
| examples/lims/screens/lims_screens.cpp | Removes explicit refresh declarations. |
| examples/lims/README.md | Documents effect-driven updates. |
| examples/lims/include/lims/models/sample_model.hpp | Declares sample action effects. |
| examples/lims/include/lims/models/auth_model.hpp | Declares login effects. |
| examples/lims/include/lims/models/analysis_catalog_model.hpp | Declares analysis effects. |
| examples/lims/include/lims/dto/verification_dto.hpp | Registers verification records. |
| examples/lims/include/lims/dto/sample_dto.hpp | Registers sample records. |
| examples/lims/include/lims/dto/result_dto.hpp | Registers result records. |
| examples/lims/include/lims/dto/offline_dto.hpp | Registers conflict records. |
| examples/lims/include/lims/dto/analysis_dto.hpp | Registers analysis records. |
| examples/common/testkit/bundle_warnings.hpp | Checks bundle validation warnings. |
| examples/common/app/tests/test_sources.cpp | Tests reconnect and event forwarding. |
| examples/common/app/client_shell.cpp | Connects sessions to push notifications. |
| examples/common/app/app_source.hpp | Declares watcher and listener interfaces. |
| examples/common/app/app_source.cpp | Implements negotiation and reconnect notifications. |
| examples/bookmarks/screens/tests/test_bookmarks_screens.cpp | Checks warning-free bundles. |
| examples/bookmarks/screens/bookmarks_screens.cpp | Removes explicit refresh declarations. |
| examples/bookmarks/README.md | Documents action effects. |
| examples/bookmarks/include/bookmarks/models/tag_model.hpp | Declares tag effects. |
| examples/bookmarks/include/bookmarks/models/bookmark_model.hpp | Declares bookmark effects. |
| examples/bookmarks/include/bookmarks/models/auth_model.hpp | Declares login effects. |
| examples/bookmarks/include/bookmarks/dto/tag_dto.hpp | Registers private-key tag records. |
| examples/bookmarks/include/bookmarks/dto/bookmark_dto.hpp | Registers private-key bookmark records. |
| examples/bank/tests/test_mcp_stdio.cpp | Tests MCP stdio responses. |
| examples/bank/src/server/main.cpp | Serves screens and MCP tools. |
| examples/bank/screens/tests/test_bank_screens.cpp | Checks warning-free bundles. |
| examples/bank/screens/bank_screens.cpp | Removes explicit refresh declarations. |
| examples/bank/README.md | Documents MCP and action effects. |
| examples/bank/memory/include/bank/models/transaction_model.hpp | Declares in-memory transaction effects. |
| examples/bank/memory/include/bank/models/payment_model.hpp | Declares in-memory payment effects. |
| examples/bank/memory/include/bank/models/payee_model.hpp | Declares in-memory payee effects. |
| examples/bank/memory/include/bank/models/loan_model.hpp | Declares in-memory loan effects. |
| examples/bank/memory/include/bank/models/customer_model.hpp | Declares in-memory account-creation effects. |
| examples/bank/memory/include/bank/models/card_model.hpp | Declares in-memory card effects. |
| examples/bank/memory/include/bank/models/auth_model.hpp | Declares in-memory authentication effects. |
| examples/bank/memory/include/bank/models/account_model.hpp | Declares in-memory account effects. |
| examples/bank/include/bank/models/transaction_model.hpp | Declares transaction effects. |
| examples/bank/include/bank/models/payment_model.hpp | Declares payment effects. |
| examples/bank/include/bank/models/payee_model.hpp | Declares payee effects. |
| examples/bank/include/bank/models/notification_model.hpp | Declares notification effects. |
| examples/bank/include/bank/models/loan_model.hpp | Declares loan effects. |
| examples/bank/include/bank/models/customer_model.hpp | Declares account-creation effects. |
| examples/bank/include/bank/models/card_model.hpp | Declares card effects. |
| examples/bank/include/bank/models/budget_model.hpp | Declares budget effects. |
| examples/bank/include/bank/models/auth_model.hpp | Declares authentication effects. |
| examples/bank/include/bank/models/account_model.hpp | Declares account effects. |
| examples/bank/include/bank/dto/transaction_dto.hpp | Registers transaction records. |
| examples/bank/include/bank/dto/payment_dto.hpp | Registers payment records. |
| examples/bank/include/bank/dto/payee_dto.hpp | Registers payee records. |
| examples/bank/include/bank/dto/notification_dto.hpp | Registers notification records. |
| examples/bank/include/bank/dto/loan_dto.hpp | Registers loan records. |
| examples/bank/include/bank/dto/card_dto.hpp | Registers card records. |
| examples/bank/include/bank/dto/budget_dto.hpp | Registers budgets and derived reports. |
| examples/bank/include/bank/dto/account_dto.hpp | Registers account records. |
| examples/bank/CMakeLists.txt | Links screens and adds MCP tests. |
| docs/superpowers/specs/2026-10-08-app-hosting-design.md | Updates push protocol description. |
| docs/superpowers/specs/2026-10-07-ui-document-design.md | Documents ui/2 semantics. |
| docs/spec/ui/service.md | Documents negotiation and watchers. |
| docs/spec/ui/interpreter.md | Documents cache effects and resynchronization. |
| docs/spec/ui/frontend.md | Documents reconnect integration. |
| docs/spec/ui/explorer.md | Documents effect display. |
| docs/spec/ui/author.md | Documents revised authoring API. |
| docs/spec/ui/agent.md | Specifies MCP adapter behavior. |
| docs/spec/testing_strategy.md | Documents bundle warning checks. |
| docs/spec/offline/offline.md | Explains replay-driven cache updates. |
| docs/spec/core/wire.md | Specifies event and watch messages. |
| docs/spec/core/shared_instances.md | Documents keyed-instance watches. |
| docs/spec/core/observability.md | Documents oversized-event metric. |
| CMakeLists.txt | Exports new push and agent headers. |
Review effort: Balanced
The refresh keys restate per document what an action's effects say once for every client, so a document that declares one is now refused, pointing at the key, rather than a second vocabulary keeping them. ui/2 and the per-document vocabulary needs of a bundle are gone; the generic client speaks ui/1 and expr/1 again, and the authoring layer emits ui/1. The redundant-refresh warning goes with the keys, and the uncovered warning asks only whether a refetch follows. The interpreter's patch, invalidation and refreshOn paths are removed; optimistic keeps the row replacement. Tests of the removed behaviour are dropped; the `at` and no-list cases move to optimistic, and the form, collection and wizard submits are checked to apply their actions' effects. Specs 5 and 8 and the ui specs describe the one vocabulary; the goldens change only their vocab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
Yaraslaut
marked this pull request as ready for review
October 19, 2026 06:17
…t to it docs/KEEPING-SCREENS-CURRENT.md is for application developers and coding agents: the three things to write (records, effects, screens), choosing an effect, what an action's result holds, what refreshes on the caller, the server and other clients, the screen rules, a checklist for every new or changed action, every startup error, load-time refusal and warning with its fix, testing, agents over MCP, the explorer and the limits. GETTING-STARTED's screen example no longer uses invalidates, and it and the README no longer say morph has no server push. ARCHITECTURE gains the section and the new namespaces and headers; IMPLEMENTATION.md and AGENTS.md make declaring effects a rule. The registry spec allows the effects macro in a header, as the rungs use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
- core/remote.hpp: a coalesced `changes` event over the envelope cap is no longer dropped. It is sent with its records as whole-type changes, and as a `resync` if even that does not fit, so a watcher is never missed; test coalesced_event_over_the_cap_goes_as_whole_types (per Copilot). - examples/common/app: the connect listeners run in a task of their own, after the bridge's reconnect handler, so a mounted query's refetch waits for the rebind instead of using the old connection's model id; the ui-hello still goes first. Test remote_source_refetches_after_the_bridge_ rebinds replays a transport's reconnect over a populated query (per Copilot). - CMake: bank's screens are built whenever the bank server is, with MORPH_BUILD_EXAMPLES off too; their tests need the generic app and are built only with it (per Copilot). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Implements spec 8 in full. Actions declare what they change, and the server tells every GUI that shows that data. Documents now declare only what a screen shows and what an edit sends; nothing lists what to refresh after an action. An agent's tool calls run through the same path, so an open GUI updates when an agent changes something.
docs/superpowers/specs/2026-10-16-server-push-design.md.docs/superpowers/plans/2026-10-18-server-push.md, 30 tasks in 9 parts, all done.What you write
From
examples/bookmarks. The full guide isdocs/KEEPING-SCREENS-CURRENT.md.1. Records, once per result type, beside the DTO:
2. Effects, beside each action that changes something:
3. Screens that say only what they show and what they send:
When the rename succeeds, this table and every other screen holding tags, bookmark rows or a bookmark's detail
refresh, on every connected client, and the same happens when an agent renames the tag over MCP.
What goes away from screens (bookmarks on master, then on this branch):
R, and its reply holds them with their keysupdates<R>: the caller's screens show the reply's rows at once; watching clients refetch the entries holding those keysR, but its reply does not hold them (delete, bulk edit, import)changes<R>: every entry holdingRrefetcheschangesInstancenoneui::refetch(q)stays for a refresh no action can declare, and.refreshEveryfor data that changes with time.What it does
Records and effects (core)
BRIDGE_REGISTER_RECORDnames a result type with a key (key<&T::m>,keyless,derivedFrom<…>,privateKeys).BRIDGE_REGISTER_EFFECTSdeclaresupdates<R>,changes<R>,changesInstanceornone. Each action's registration records where its result holds which record types.EffectsRegistrationError. This includes aderivedFromsource that is not a registered record, which would otherwise never refresh the record derived from it.deriveChangescomputes the changes an execute made. PastkMaxChangeKeyskeys, a change becomes a whole-type change.The server-to-client channel
eventenvelope (changes,resync), which clients route before matching replies.RemoteServerwrites to a connection throughsetConnectionWriter,postandconnectionDrained.resyncfollows the drain. Replies are never held.sendTimeout.Watching and fan-out
watchandunwatchare core kinds. Admission is per principal and token, an instance watch also needsauthorizeInstance, and a connection may watch at most 4096 topics.privateKeysrecords lose their keys for any other principal.resync.The client cache
Session::actionSucceeded. This covers mutations and the submits of forms, collections and wizards.updates Rpatches rows by the record's key field, at the catalog's paths, with no fetch.changes RandchangesInstancemark the entries that hold the record, or that were read through the instance.latestreply only marks.refetchAfter): that entry fetches once more, so no change is lost.Session::deliverEventapplieschangesevents, filtered by the event's keys, and answersresync.SessionOptions::watcher, built withwatcherOver.Session::reconnected(), and everyresync, re-send the whole watch set withreplaceand mark every entry stale.AppSource::onConnectedis a new seam. On every reconnect, the generic client re-sendsui-hello, re-watches, and forwards events.changestoo, so the caller's own reply marks it in process as well as remotely.Documents
invalidates,patchandrefreshOnare refused: an action's effects say what it changes, once, for every client.optimistic,refreshEveryand therefetchcommand stay.watchof a query that runs a mutation with effects.The example rungs. Bookmarks, bank and lims declare records and effects beside their actions. Their screens declare no refreshes, and each rung's bundle loads without refusals or warnings.
Documentation
docs/KEEPING-SCREENS-CURRENT.md, a new guide for developers and coding agents: what to write, choosing an effect, what refreshes when, a checklist for a new action, every error and warning with its fix, testing, and agents over MCP.GETTING-STARTED.md,ARCHITECTURE.md, the README,examples/IMPLEMENTATION.mdandAGENTS.mdpoint to it and no longer show refresh keys or say morph has no server push.The MCP adapter
ui::McpAdapterspeaks MCP's JSON-RPC 2.0 over an ordinary server connection, with one tool per permitted action and anevaluatedry run.serveStdiodrives it over MCP's stdio transport.--mcp-stdio, beside its WebSocket clients, and now also serves bank's screens through aUiService.Changes from the plan
serveStdiotakes the adapter's owner, aMainThreadExecutor, and pumps it while it waits.McpOptionsgainscallbacks, the serial owner the adapter's completions are delivered on.applyChanges(ChangesBody)serves events only, because a patch needs rows, and aChangesBodycarries only keys.ui/2is folded intoui/1: the refresh keys are refused there instead of in a second vocabulary, so there is no per-client choice of documents. Spec 5 §12 says a key leaves a vocabulary by being refused, never ignored.--mcp-stdioidentity comes fromMORPH_MCP_PRINCIPAL, because bank's demo authorizer vouches for a principal and has no token.MORPH_MCP_TOKENis carried as the token.Verification
Measured on Linux (Arch), clang 23, Debug, with net, Qt, the ladder and bank on, at
40740124; the later commits re-ran the full suite (all pass), Doxygen (clean), and a local branch-coverage run ofinclude/morph/core(94.15% against the 94% floor, without the examples):Full build: OK.
ctest: 4014 of 4016 pass. The two failures were timeouts under
-j6on a loaded disk. Both pass alone:ListBookmarks … nextCursorin 0.14 s, andApp teardown survives fetch dispatches still outstandingin about 90 s. On master's own build that test takes 81 s on the same disk, so the load is the machine's, not the branch's.Doxygen (
WARN_AS_ERROR): no warnings.GCC: every non-Qt translation unit under
examples/,src/andtests/(378) compiles with GCC and CI's gcc-release flags. With a fix undone, the check flags the error.Mutation checks: each new behaviour's tests were run with the behaviour broken and fail as they should, including:
resyncpaths;The net backlog test fails without the hold-back. The two-client test over
netfails with B's events dropped.CI on
40740124was green on every build and test job, Windowsclandclang-cland the sanitizers included. The exceptions were clang-tidy-diff and the coverage gate (core's branch coverage under its floor), both fixed in the commits after it.Not verified locally:
MORPH_CLIENT_ONLY, beyond its guard probes in the normal build.Known limitations
updateseffect cannot express a patch whose reply replaces its row under a new key. Lims's re-capture declareschangesand refetches.sendTimeout).ui-helloanswersevents: true.deriveChanges.🤖 Generated with Claude Code
https://claude.ai/code/session_01A4hwcANrTbPYnqmWX4iyMZ