Files
trail-mate/docs/specification/CHAT_WORKSPACE_MODEL_SPEC.md
T
liu weikai f2ff45283d Refactor Reticulum runtime ownership boundaries
Add Reticulum runtime budget, announce scheduling, deferred discovery, RX telemetry, and adapter scratch owners.

Route notification feedback through a platform notification runtime, keep product calls on Sideband LXST, and classify Reticulum network projections.

Unify LoRa TX budget handling across protocol actions, ACK retry, app sends, and MQTT downlink relay; document frozen runtime ownership rules and add smoke coverage.
2026-07-19 22:41:51 +08:00

6.1 KiB

Chat Workspace Model Specification

Purpose

ChatWorkspaceModel is a presentation-layer ViewModel.

It owns UI-local workspace state only:

  • selected conversation
  • conversation list offset
  • message list offset

It does not own:

  • ChatService
  • ContactService
  • TeamService
  • MeshSession
  • message repository
  • pending queue
  • ACK tracker
  • retry state
  • failure inference
  • read/unread ledger

Pattern

This model follows:

  • MVVM / Passive View
  • Source/Sink Port
  • Immutable Snapshot
  • CQRS Read/Command split

Snapshot Flow

Renderer calls:

ChatWorkspaceModel::snapshot()
    -> IChatPresentationSource::buildChatWorkspaceSnapshot(request, out)

The returned ChatWorkspaceSnapshot is an immutable value object for UI rendering.

Action Flow

Renderer calls:

ChatWorkspaceModel::selectConversation(id)
ChatWorkspaceModel::sendMessage(text)
ChatWorkspaceModel::markRead(id)

The model forwards actions to IChatActionSink.

Read State Authority

ChatWorkspaceModel::markRead(id) is a UI intent. It is not the authoritative read-state mutation.

The authoritative read/unread owner is ReadStateLedger, as frozen by RUNTIME_OWNERSHIP_BOUNDARY_FREEZE.md.

Required flow:

Renderer
    -> ChatWorkspaceModel::markRead(...)
    -> IChatActionSink
    -> app/runtime read command
    -> ReadStateLedger commit or pending result
    -> ConversationProjectionStore / ChatPresentationSource
    -> unread badge projection

The workspace model may optimistically keep local selection and offsets, but it must not claim durable read success. Conversation index entries, SD file headers, unread counters, and app badges are projections of the ledger. They may cache or mirror read state, but they must be rebuildable from MessageLedger + ReadStateLedger.

Renderers must not hide an unread badge as the source of truth. They may only show the projection returned by IChatPresentationSource, or a clearly pending state produced by the read command path.

Mark-read failure semantics must stay explicit:

  • committed: projection may clear the unread badge.
  • pending: projection may show a temporary pending read state.
  • failed: projection must not pretend the conversation is read.

This rule applies equally to Meshtastic, MeshCore, and Reticulum conversations. The read reference must preserve protocol identity; a bare message id or peer id is not a valid cross-protocol read key.

Protocol Send Eligibility

Conversation protocol and active send protocol are separate facts.

ConversationId.protocol identifies what protocol produced or owns the conversation. The active runtime protocol identifies which transport can send right now. If they differ, the conversation remains visible and selectable, but it is read-only for chat commands.

Required behavior:

  • Read paths must continue to show cross-protocol conversations and messages.
  • Compose, reply, retry, and sendMessage must be disabled or rejected for cross-protocol conversations.
  • IChatActionSink adapters must map SendMessageView.conversation back to a full core chat::ConversationId; they must not drop the protocol and send by bare channel + peer.
  • Devices that lack a channel creation entry may add one in their renderer, but renderers that already provide channel selection must not grow a second duplicate entry.

Optimistic Selection

selectConversation(id) uses optimistic UI selection.

Semantics:

  1. Validate the presentation ConversationId.
  2. Update local selected_conversation.
  3. Reset message_offset.
  4. Notify IChatActionSink::selectConversation(id).
  5. Do not roll back local selection if the sink returns failure.

Rationale:

  • selected conversation is presentation-local state.
  • sink selection is a synchronization hook for business-side side effects.
  • unsupported conversations may still be visible/selectable in UI.

Message Paging

ChatWorkspaceRequest::message_offset is reserved for future message paging.

ChatPresentationSource currently ignores message_offset and returns the recent message window.

This is intentional. The closeout phase must not change ChatService storage or paging behavior.

Renderers must not assume message_offset is already honored by the chat read projection.

Send Feedback

ChatWorkspaceModel::sendMessage(...) submits a command and returns only local command acceptance/rejection.

It must not:

  • wait for ACK
  • infer final send success
  • show send success/failure feedback
  • depend on the active page after the command is accepted

Final outgoing send feedback is produced from runtime delivery result events as specified by CHAT_DELIVERY_FEEDBACK_SPEC.md.

Source/Sink Adapter Contract

ChatPresentationSource is the product chat read projection adapter. It may read ChatService and ContactService, and may use chat_presentation_adapters to map core chat types into ui_presentation rows.

It must not:

  • send messages
  • mark conversations read
  • mutate ChatService
  • access LVGL widgets
  • access radio, mesh adapters, PKI, or packet builders

RuntimeChatActionSink is the runtime command adapter. It may translate UI actions into ChatService commands.

It must not:

  • build ChatWorkspaceSnapshot
  • format UI labels
  • access LVGL widgets
  • inspect renderer state
  • build radio packets or perform PKI logic

Pending / Failure

ChatWorkspaceModel must not own pending messages, ACK tracking, retry state, or failure inference.

Pending/failure projection must flow through:

ChatService / MeshSession / ACK tracker / pending store
    -> IChatPresentationSource
        -> MessageRow.delivery
        -> MessageRow.failure

Until structured failure ownership is explicit, the compatibility projection is limited to the coarse chat::MessageStatus available from ChatMessage.

Team Chat

Team chat is not part of the generic DirectPeer/Channel path.

Team rows use:

ConversationKind::Team

Team send/read/projection semantics are handled by a dedicated Team Chat Presentation phase.

Contract Role

This file is an Architecture Decision Record and ViewModel contract. It constrains later implementation; it is not only descriptive documentation.