Conversations and thread calls
Document: SPEC-013. Revision: 0.7. Date: 2026-11-02. Lifecycle: Approved for D-067–D-069, D-075, D-080 D-157, D-162 and D-172 scope. Supersedes: SPEC-009 envelope field lists and SPEC-012 navigation only as described below.
Authority and behaviour
D-206 selects the native Qt presentation exception in SPEC-005’s native muted conversation fields section: repeated root context, thread backgrounds, own-message marker, inline metadata and compact overlay controls supersede the native portions of D-067/D-075/D-080 only. Shared web/mobile clients retain the presentation below. Ordering, actual reply references, history eligibility, delivery semantics, drafts and voice authority remain unchanged.
D-067 confirms selecting a channel to view its text and activating it again while viewing that chat to join/move voice. Selecting a different channel or returning from a settings workspace only opens chat. Activation while already in that channel's call does not leave it; explicit Leave remains available. Text-only channels have no channel call. Repeated held keys, disclosure controls, people and context menus do not activate voice.
D-067 also confirms consecutive messages from the same account grouped under one identity heading, a distinct application colour for one's own messages, and a small timestamp after a gap strictly greater than two minutes. Use account IDs, never nicknames, for grouping. Start a new group on a different author or local calendar date. Show the group's initial time, full timestamp on inspection, and a date separator on date changes. Retain YOU and shared-history/error labels. Presentation retains D-065's compact layout and D-066's system-selected light/dark palettes and forced colours.
D-068 confirms persistent conversation replies expanded directly beneath their original message. The main composer starts a new conversation. Each message offers Reply; replies to replies stay in the same one-level conversation. Reply editors are inline, keyboard accessible and retain unsent text on errors. Collapsing preserves that draft during the current channel view. Navigating away does not send it. Replies do not reorder original conversations. Missing originals, including expired or pre-admission originals, show an unavailable marker rather than fetching otherwise ineligible text. Counts describe loaded eligible replies only. This does not add edits, deletions, attachments, notifications or read receipts.
D-069 confirms separate voice for a conversation, isolated from both its parent channel and other conversations. A Join voice action is available on text conversations, including text-only channels; it does not turn the text-only channel itself into a voice channel. The existing one-active-call-per-account rule, explicit move fence, fresh-press PTT, encryption, bounded relay lifetime and moderation deadlines apply. Thread calls inherit parent access, account roles, mute, shared gain, encryption and PTT policy. There is no independently administered member list. Call status/participants appear with the conversation; the persistent voice strip names the parent and conversation number. Joining a call does not change the selected chat or send a message.
D-162: Display submitted text immediately as a local outgoing row with Sending status, including inline replies under their original root. Server acknowledgement changes the status to Sent; this means accepted by the service, not read or received by another person. A definite rejection shows Not sent and preserves the draft; an ambiguous outcome shows Confirmation pending and retains the existing original-command retry flow without creating another operation. Match the signed envelope ID, author/device and channel when authenticated history replaces a local row. Repeated identical text is not a match. Retain server sequence ordering once known without advancing the history cursor from a send acknowledgement. Local rows are memory-only, cleared on view/account context change, and never grant history access or enable reply/call actions before canonical history arrives. Existing drafts remain protected by their current storage rules. No offline send queue or delivery/read receipts are added.
Shared protocol and persistence
The following are technical completion choices under D-024/D-049, not additional individually stated human preferences. Servers advertise message-threads/1 and thread-voice/1 in /api/info; clients gate the respective actions on these capabilities. Existing unthreaded envelopes remain unchanged. Older clients can verify signed additive fields and display replies chronologically; updated clients do not send reply fields to older servers.
MessageEnvelope.replyTo, when present, is exactly {root: positive safe integer, parent: positive safe integer}. Both are host sequence numbers in the same channel; parent >= root. parent identifies the message being replied to, and root equals its replyTo.root or its own sequence. The field is included in the outer signature and encrypted inner context; recipients compare them exactly. Server-readable messages bind it through the signature. History copies/recovery authenticate the complete original envelope digest. Existing SQL envelope storage needs no table migration.
The host accepts replies only to retained messages within the sender's eligible membership intervals. Missing, expired or ineligible targets produce the same thread_unavailable error without disclosing their contents or existence. Retries retain normal operation/message idempotency. A visible reply can anchor another reply after the original expires; it cannot expand any history entitlement. A reply's root and parent must precede its assigned host position. Existing message quotas, size limits, retention, authentication and device-recipient rules are unchanged.
Authenticated POST /api/rooms/:channel/threads/voice with exactly {message: positive safe integer} resolves an eligible retained message to its root and returns {room: RoomInfo, root: number}. It lazily creates or reuses one internal voice-only room UUID per channel/root inside a transaction. It atomically raises the database compatibility marker to at least schema 3 when creating the first internal call; SPEC-015 schema 4 is never lowered. Schema 3 uses the same tables as schema 2; the new Store reads both, and older servers reject schema 3 rather than exposing internal calls as independent channels. Backend rollback requires an explicit compatible migration; changing the marker alone is unsafe. It does not start capture or move voice itself. Clients then use the existing scoped voice APIs. Internal rooms carry thread: {parent: channelID, root: number}, are hidden from channel listings and cannot receive independent administration, membership, message or device-history commands. Only state and voice endpoints are exposed. IDs and lease/grant room bindings keep audio separate without a relay protocol change.
Thread call membership is derived from current enabled parent membership plus at least one retained eligible message in that conversation; an already active participant remains eligible while their call is live if text subsequently expires. Parent policy changes invalidate derived call revisions/enforcement in the same transaction. Parent removal/access revocation ends the participant's derived voice session. Thread room properties are read from the current parent, never an independently editable copy. The selected channel's sync response contains threadCalls: [{root, room, participants: VoiceMember[]}] for visible live thread calls, with no hidden call disclosures. Own remote voice state includes its display name. Empty internal rooms are removed once the conversation has no retained messages and no active voice. Their creation is bounded by the existing retained-message quota; they do not consume the 100 user-created-channel quota. Preparation/session/enforcement records keep their existing bounded expiry.
Message indentation (D-075)
Chat sender identities use regular weight and the accessible muted text colour, superseding bold identity headings in chat only. Keep message content at the normal stronger text colour (or the existing own-message accent). Indent message text 12 CSS pixels from its sender heading. Continuations use the same text alignment; small gap timestamps follow the text inset. Apply this inside both main messages and thread replies without adding another author heading or changing thread nesting. This is a presentation constant, not a user setting. Browser checks compare heading/body positions in light, dark and narrow layouts.
Message actions (D-080)
Shared history, Reply and thread Join/Move voice controls align with the message body’s 12-pixel inset. On pointer devices they appear only while hovering that individual body/action area, not its sender heading or another message. Keep their space reserved so revealing controls does not move the text. Keyboard focus reveals the controls when reached by Tab; devices without hover show them so touch users retain access. Reply counts/expansion controls and active call participant status remain visible. Apply this to original messages, grouped continuations and inline replies. Shared history remains a provenance label, not a new action.
Pinned comments (D-172)
CONFIRMED 2026-10-27: the human explicitly grants the pinned message to current and future channel members, including members whose normal history starts later. An ordinary text-capable channel has at most one pinned original message; local server owner/moderators may pin, replace or clear it. Private conversations/internal thread rooms do not gain this server moderation feature. This is a one-message access exception, never an expansion of ordinary history intervals, replies or thread-call eligibility. Retention remains the original message's expiry; expiry/deletion/clear removes the visible pin and its copy records. Existing readers cannot be made to forget previously read content.
The channel-pins/1 completion profile stores an independent pin ID and original message sequence/digest, host/channel binding and original expiry. Pin changes use revision-checked idempotent commands and do not change voice membership revision. The source must be a retained original the moderator can authenticate/read. The server retains its original signed envelope and author projection; clients display author plus the approving moderator, clearly identifying the moderator-approved pinned copy. Conditional service notices follow beneath it.
For E2EE, the pinning moderator signs the complete content and pin context inside encryption. Signed per-device deliveries use the existing authenticated box primitive; the server receives no plaintext, plaintext hash or usable content key. Initial delivery covers current approved member devices. Any currently authorized device that verifies/decrypts that moderator assertion may automatically re-encrypt the unchanged assertion to newly admitted approved devices. Recipients authenticate the moderator assertion independently of the copying device, preventing a member from changing the approved text. Historical moderator authorization is captured at pin acceptance; ordinary identity/roster trust and required channel encryption remain. A newcomer may wait while all devices holding a readable copy are offline; an existing authorized device's return completes distribution without another approval or password. Server-readable pins use the verified original directly and have no such key wait.
Copies are first-write/idempotent per target and pin; a current target may reject its own invalid delivery, excluding that copying device for this target/pin so an invalid first copy cannot permanently poison access. Revoked/nonmember devices cannot fetch, upload or receive new copies. Server membership and approved device rosters are checked at every copy operation; the exception never exposes another message. The increment review records implementation and delivery evidence.
Configuration
D-172 pins start absent, with at most one per ordinary channel. No new user setting is introduced. Maximum content, approved-device and request sizes inherit SPEC-009/011 and existing LIMITS; delivery batches contain at most 50 unique approved devices. Each pin/target retains one delivery and at most 50 rejected copying-device IDs. Copies retain the original expiry, never extend it, and are pruned when their original expires or disappears. Automatic copy attempts are bounded to one batch per selected pin/recipient set and retried after ten seconds on transient failure. These are fixed protocol/resource bounds.
D-157’s 2026-10-24 timestamp request adds parenthesized elapsed time after each already-visible clock time, including replies and gap timestamps: (0 minutes ago), (5 minutes ago), (1 hour ago), (1 day ago). Use completed minutes below one hour, completed hours below one day, and completed days thereafter; singular at one, plural otherwise. Clamp future timestamps to zero minutes. Preserve the exact datetime and full-date tooltip, existing grouping and history timestamps. One shared display clock refreshes every 30 seconds while the conversation is mounted, without network requests; this is a fixed rendering interval, not a preference.
No new user or deployment setting is introduced. The strict 120000 ms intra-group timestamp threshold is a fixed human requirement. Reply references, expanded state and drafts are message metadata or transient client state, not configuration. Parent policy and existing media/retention defaults remain owned by SPEC-004/009/012; a text-only parent's existing stored 60-second PTT default applies to its thread calls. Palette roles, including own-message foreground/background, belong to SPEC-005. Expanded state starts collapsed unless a saved reply draft exists. SPEC-015 owns protected per-connection/channel/thread draft persistence.
Testing
D-172 units cover moderator authority, wrong/expired/pre-admission originals, independent pin replacement/clear and command idempotency, future membership without surrounding history access, encrypted copy signatures/context/target checks, tampered member copies and target rejection/recovery, revoked/nonmember devices and original expiry. Browser integration covers Pin/Unpin controls, placement before status, automatic future-member access while the moderator is offline, bounded encrypted waiting/recovery, account/channel isolation and retained ordinary history boundaries.
D-162 units check local/history identity matching, retry envelope changes and distinct identical submissions. Browser tests hold acknowledgement and history independently, assert immediate Sending then Sent, no duplicate on history arrival, explicit rejection/unknown states with retained drafts, reply placement and context isolation.
Elapsed-time unit tests cover future/zero, singular/plural minutes, hour/day boundaries and multiple days. Browser verification checks the bracketed label alongside the clock time and preserves its exact datetime/tooltip.
Unit tests cover account/date grouping, exact two-minute boundaries, unavailable roots, stable root ordering, and signed/encrypted reply-context tampering. Server tests cover reference shape, wrong root, nonexistent/expired/out-of-period parents, visible replies after root expiry, readable/E2EE persistence, reload, shared-history authentication, capability advertisement, canonical call resolution, hidden listings, inherited mute/gain/access, revocation, text-only conversation calls, and isolated lease room bindings. Existing linked-device, history-interval and move-fence tests remain applicable. Browser integration checks body-only hover, action alignment, stable geometry, keyboard reveal and non-hover access, then covers inline reply/cancel/send/collapse, failed drafts, first/second channel activation, repeated activation, two independent thread calls, parent-call separation and explicit PTT movement. Physical desktop/audio acceptance remains distinct from automated browser/relay evidence.
Acceptance criteria
- Admins/moderators can pin, replace and clear one retained verified channel comment; ordinary members cannot change it.
- Current and future admitted members can read that one pin without receiving otherwise ineligible history or replies. E2EE copying verifies the moderator-approved text and needs no user approval; an offline-holder wait is explicit and recovers automatically.
- Pin changes preserve live voice/PTT state, original retention, current membership/device authority and host/channel separation. Unpin/replacement/expiry removes the old visible pin.
- Submitting a channel message or reply displays it before the network acknowledgement; acknowledgement changes its status while history is delayed. Canonical history replaces it once without matching solely by text; rejection and uncertain outcomes remain visibly distinct and drafts survive.
- Each visible message clock time includes elapsed minutes/hours/days in parentheses, updates without new messages, and retains the original exact timestamp for inspection.
- D-080: Shared history, Reply and thread voice controls are hidden outside the individual message body on hover-capable devices, aligned with its text when revealed, and reachable by keyboard and touch without shifting chat.
- R-079: selecting another channel opens chat without moving voice; reactivating the displayed chat joins/moves channel voice once and never toggles it off.
- R-080: consecutive messages by one account share a heading, own messages are distinguishable in both system-selected palettes, and only gaps greater than 120000 ms add intra-group timestamps.
- R-081: replies persist across reload and expand beneath their root; replying to a reply remains there; errors preserve the draft; unavailable originals do not bypass history admission or retention.
- R-082: a thread call's participants can hear each other and cannot hear the parent or another thread; switching calls requires the existing fence and a fresh PTT press.
- Parent mute, gain, device revocation and membership removal affect thread calls under the existing enforcement deadlines; no independently editable thread permissions are introduced.
- Existing unthreaded history, encryption modes, device history copies and old-client verification remain compatible; unsupported servers receive no new reply/voice actions.