Reactions, replies, read-receipts, edits, deletes, forwards and channel sends
shared one bare `{ ok: true, sent: true }` case, so none of them rendered on
the demo — the UI derives reaction chips and reply quotes from the message
store, and there was nothing in it to derive from.
Each now mirrors its daemon counterpart. Reactions/replies/receipts push typed
messages carrying the { sender_pubkey, sender_seq } target key Mesh.vue's
reactionIndex and replyTargetPreview read. Edits rewrite the text and set
edited_at; deletes tombstone IN PLACE (plaintext, typed_payload.deleted,
message_type 'delete') because that is what mesh/mod.rs apply_local_delete
does — it does not remove the row.
Edits and deletes go through a per-session overrides overlay keyed by
sender_seq, because mesh.messages rebuilds its seed array on every read, so
in-place mutation would only ever work for messages sent this session.
mesh.refresh and mesh.reboot-radio stay acknowledgements on purpose — the
daemon's handlers have no message-store effect either — with a comment saying
so, so a later reader does not "fix" them into divergence.
Also completes the phase bookkeeping for 01-02/03/11/12/13/14/15 and lands the
orphaned 01-12/01-14 SUMMARYs.
Verified: parity harness 17/17 live assertions; full frontend suite 102 files
/ 822 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mock-backend.js's per-session store (mesh.dynamic/blobs) and the attachment-parity handlers landed in c2ce71c6 (send-content-inline / send-content / fetch-content / transport-advice)
Ten previously-missing mesh/federation RPC handlers, each mirroring and citing its daemon counterpart
Stateful chat mutations (reaction, reply, read-receipt, edit, delete, forward, channel) visible on the next mesh.messages read
scripts/mock-rpc-parity.mjs + npm run test:mock-parity — a single command that fails when the demo drifts from the UI's call surface
demo
mesh
federation
added
patterns
Parity harnesses must match call syntax (`method: '<x>'`), not bare string literals. The same dotted names are used as resource-cache keys in the UI (`key: 'federation.nodes'`), so a literal scan reports phantom gaps that can never be closed — which would make the harness permanently red and therefore ignored.
Mirror-don't-invent: every mock handler carries a comment naming the Rust file and function it mirrors, so a future reader can re-check parity instead of guessing what the demo is 'supposed' to do.
created
modified
neode-ui/scripts/mock-rpc-parity.mjs
neode-ui/mock-backend.js
neode-ui/package.json
The STATIC stage matches `method: '<x>'` rather than every `'mesh.*'`/`'federation.*'` string literal as the plan specified — see Deviations. This is what let KNOWN_GAPS actually reach zero.
Edits and deletes are applied through a per-session `overrides` overlay keyed by sender_seq rather than by mutating a message array, because the demo's seed message list is rebuilt on every mesh.messages read. The overlay makes edit/delete observable on seeded messages too, not just ones sent this session.
Delete tombstones in place (plaintext '🗑 message deleted', typed_payload { deleted: true }, message_type 'delete') because that is precisely what mesh/mod.rs apply_local_delete does — it does not remove the row. A comment says so, since 'simplifying' it to a splice would be a silent divergence.
mesh.refresh and mesh.reboot-radio were deliberately LEFT as bare acknowledgements: the daemon's handlers have no message-store effect either, so giving them demo state would be divergence rather than parity. The comment records this so a later reader does not 'fix' them.
mesh.peers and mesh.contacts-list now read from one shared DEMO_MESH_PEERS constant, so the two can never disagree about who is on the mesh — the daemon merges contacts over the same peer map for the same reason.
An outbound/'sent' pending request was added to the demo seed. federation.cancel-request faithfully rejects anything that is not outbound-and-sent, so without such a request the demo's cancel button could only ever produce an error.
FED-04
id
description
requirement
verification
human_judgment
D1
Every mesh.*/federation.* RPC the UI calls has a mock handler — the demo never answers a UI call with 'Method not found'
Renaming a mesh peer on the demo persists across a contacts-list read
FED-04
kind
ref
status
integration
…LIVE stage — contacts-save then contacts-list round-trips the alias
pass
false
id
description
requirement
verification
human_judgment
D3
Reaction, reply, edit, delete and forward mutate demo state and are visible on the next mesh.messages read
FED-04
kind
ref
status
integration
…LIVE stage — six mutation assertions, each re-reading mesh.messages and checking the UI-expected shape
pass
false
id
description
requirement
verification
human_judgment
D4
The parity check fails when a UI-called method has no mock handler
FED-04
kind
ref
status
integration
Fail-first proof: disabling the mesh.clear-all case → exit 1, 'no mock handler for mesh.clear-all'; restored → exit 0 twice consecutively
pass
false
id
description
requirement
verification
human_judgment
D5
The harness leaves no stray listener behind
FED-04
kind
ref
status
integration
Two consecutive runs both exit 0 (child killed in a finally block; ephemeral port via MOCK_BACKEND_PORT)
pass
false
75min
2026-08-01
complete
Phase 1 Plan 2: Demo/Real Mesh RPC Parity (FED-04) Summary
Closed the ten mesh/federation methods the demo answered with "Method not found", made the six ack-only chat mutations actually mutate demo state, and replaced "squint at the browser console" with a single command that fails when the demo drifts.
Performance
Duration: ~75 min
Completed: 2026-08-01
Tasks: 3/3
Files modified: 3 (mock backend, new harness, package.json)
Accomplishments
Ten new handlers, each citing the Rust it mirrors: mesh.contacts-list/-save (typed_messages.rs), mesh.clear-all (status.rs), mesh.schedule-message/list-scheduled/cancel-scheduled (assistant.rs + scheduler.rs), mesh.assistant-status/-configure (assistant.rs), federation.cancel-request and federation.notify-did-change (federation/handlers.rs).
The chat mutations are no longer bare acknowledgements. Reactions, replies and read-receipts push typed messages carrying the { sender_pubkey, sender_seq } target key the UI's reactionIndex/replyTargetPreview read; edits rewrite the text and set edited_at; deletes tombstone in place; forwards copy to the destination peer; channel sends are channel-addressed.
mesh.peers and mesh.contacts-list share one DEMO_MESH_PEERS list, and the peer with no pubkey_hex is omitted from contacts exactly as the daemon's if let Some(pk) guard omits it.
scripts/mock-rpc-parity.mjs runs a static cross-reference then boots the mock on an ephemeral port and drives 17 live assertions. No || fallback escapes anywhere in it — a failed spawn or fetch fails the run rather than producing a green run that measured nothing.
Task Commits
Tasks 1 + 2: contacts round-trip, harness, and the ten missing methods — b8979f36 (feat)
Task 3: make the message-mutation stubs mutate demo state — committed with this SUMMARY
Deviations from Plan
The STATIC scan matches call syntax, not every string literal — and the plan's gap list had two false positives
Found during: Task 1, building the static cross-reference
Issue: The plan specifies scanning src/** for "every 'mesh.<verb>' / 'federation.<verb>' string literal". That over-reports badly: Mesh.vue and Federation.vue use the same dotted names as resource-cache keys (key: 'mesh.self-did', key: 'mesh.transport-status', key: 'federation.nodes', key: 'federation.dwn-status'), and stores/sync.ts invalidates by the same strings. None are RPC methods. A literal scan reports 18 gaps where 10 exist.
Two of those phantoms — federation.nodes and federation.dwn-status — are named in the plan's own Task 2 gap list. They are not RPC methods and the daemon's dispatcher has no such routes; Federation.vue uses them purely as cache keys. Implementing them would have added demo-only endpoints the real node does not serve, which the plan's own transparency prohibition forbids.
Resolution: The scan matches method:\s*['"]…['"], i.e. an actual call site. The gap list becomes exactly the 10 real methods, KNOWN_GAPS is empty, and the harness is genuinely failable — proven by disabling a case and watching it exit 1.
Files modified:neode-ui/scripts/mock-rpc-parity.mjs
Added an outbound pending request to the demo seed
Found during: Task 2, implementing federation.cancel-requestIssue: The daemon only permits cancelling an outbound request in sent state. The demo seed contained a single inbound/pending request, so a faithful handler could only ever return an error and the cancel path was unexercisable.
Resolution: Added preq-demo-out-1 (outbound, state sent) to pendingPeerRequests. The handler stays faithful — it still rejects non-outbound and non-sent requests with the daemon's own message.
Files modified:neode-ui/mock-backend.js
Edits/deletes use an overlay rather than in-place mutation
Found during: Task 3
Issue:mesh.messages rebuilds its seeded message array on every read, so mutating "the message" in place would be lost immediately for any seeded message and only work for messages sent in this session.
Resolution: A per-session overrides map keyed by sender_seq, applied over the merged list on read. This matches the daemon's matching rule (own-Sent message by sender_seq) and makes edit/delete observable for seeded messages too.
Files modified:neode-ui/mock-backend.js
Known Stubs
mesh.assistant-status reports ollama_detected: false, claude_available: false and an empty model list. That is the honest answer for a browser demo with no local model — the UI's "not detected" path is what a visitor should see, and fabricating a model list would be exactly the demo-only divergence this plan's prohibition forbids.
Threat Flags
None. This plan touches only the demo/mock backend and a dev-time harness; no production code path, endpoint or trust boundary is involved. The T-01-SC package-install threat does not apply — no dependencies were added (the harness uses only node: builtins and global fetch).