Sloppy Voice DevelopersSloppy Voice ↗

Room extension implementation profile

Document: SPEC-012. Revision: 0.9. Date: 2026-11-07. Lifecycle: implementation profile under D-055/D-058, selecting the reviewed P-027 completion scope. Supersedes SPEC-009's singleton room restriction for clients advertising rooms/1. SPEC-001 owns room properties, SPEC-003 encryption, SPEC-004 media, SPEC-006 history, SPEC-005 the client and SPEC-008 server permissions. Existing lobby wire payloads remain compatible.

Control-message extension (D-228)

SPEC-028 adds signed nonVisible channel envelopes and explicit API history/sync retrieval. It supersedes the assumption that every accepted channel message appears in ordinary history projections; existing eligibility, retention, ordering, encryption and permission rules remain authoritative. Schema 10 preserves schema-9 storage and refuses incompatible predecessor-server rollback.

Conversation extension — 2026-10-19

SPEC-013 refines tree activation: first selection opens text, and reactivation of the displayed chat explicitly joins/moves voice. It adds internal parent-governed voice rooms for conversation calls, hidden from channel navigation and channel-count limits. Ordinary room creation, membership, encryption and history defaults remain unchanged. SPEC-013 adds the schema-3 compatibility marker on first thread-call creation, preserving the schema-2 tables and records while making older-server rollback fail closed.

Scope and storage

Support owner-created text, voice and combined rooms, with the reviewed public/private access and immutable e2ee/server_readable choices. Meetings remain deferred. Keep the existing lobby identity and its encrypted combined-room settings, account UUIDs, password verifiers, original device anchors, tokens, membership intervals and message bytes. Migration from SQLite schema 1 to 2 is one transaction: move room counters into room records, split membership/access/moderation into room-member records keyed by room/account, and add the room to the message primary key. Old message IDs and per-room positions remain unchanged. Schema 1 code must refuse schema 2 rather than run singleton queries against it.

New room IDs are server-generated UUIDs. Metadata fields and their validation/defaults are owned by SPEC-001 and the accepted room review. Creation uses the existing purpose-bound command ticket with action room.create; it rechecks current owner authority but does not depend on the selected conversation's revision, because it creates an independent room. Other mutations retain their revision checks; creation, creator admission, initial access grants and the operation result commit atomically. Retrying the same ticket returns the same UUID. Access grants do not admit selected members or expose their history before joining. One owner remains a domain role; existing owner membership protection remains. Role changes and device-roster changes invalidate affected room revisions. A moderator has no implicit access to an inaccessible private room.

Routes and authorization

GET api/rooms lists authorized metadata and the caller's admission/can-join flags, plus their active voice target (if any). Public rooms are discoverable to enabled local accounts; private rooms appear only to the owner or an admitted/granted account. Unknown and private-inaccessible IDs both return 404. An explicit access denial blocks code-free public and private admission. All content/history operations still require current admission.

New scoped routes are api/rooms/<room>/state, history, accounts, tickets, commands, voice/* and devices/history[/status]. Existing unscoped variants remain aliases for lobby only. Account login, registration, link/recovery management and logout remain domain routes. Tickets bind actor, device, room, command payload, expiry and selected service audience. Stored operation results cannot bypass current permission checks. History copies bind the actual room and preserve original eligibility, positions, signatures and expiration under SPEC-011.

The state response adds room metadata and identifies the account's active voice room separately from the selected conversation. Text navigation never changes that voice target. The client maintains a separate API context for active voice and resets message pagination when selecting a different conversation. Text-only rooms reject text-inapplicable voice requests; voice-only rooms reject text and history requests. A private access grant lets a member choose Join room directly without a code.

Text and media formats

E2EE text retains the SPEC-009 envelope and SPEC-011 recipient expansion; both outer signature and encrypted inner body bind the actual room ID. Server-readable text retains the common envelope fields, uses an empty boxes array, and adds encryption: server_readable plus bounded text. Its Ed25519 signature covers all fields. The server requires the exact variant for the stored immutable room mode; no implicit downgrade or mixed plaintext/ciphertext payload is accepted. The client clearly labels server-readable rooms and stored readable content uses the existing retention/eligibility policy.

Media grants accept lobby or a UUID room and retain current session/device, revision, generation, restart, mute and lease checks. An absent encryption means legacy E2EE. A server-readable media grant explicitly carries encryption: server_readable and the sender's Ed25519 signKey. Relay peers remain capped across the whole test relay, not per room. Cross-room forwarding is rejected independently of device/session identity.

E2EE audio retains its existing encrypted packet and binds the actual room inside the authenticated ciphertext. Server-readable audio uses the signed packet {profile, room, source, target, session, revision, sequence, encryption, pcm, signature}, with profile voice-chat/1, encryption server_readable and base64url 320-sample little-endian PCM. Sign the canonical JSON of all fields except signature using the existing device key. The relay verifies the signature using the grant key and enforces the current room/mode/session before forwarding; the recipient independently verifies the current participant key, context, replay window, mute and sample count. Transport remains encrypted with WebRTC DTLS. E2EE rooms reject readable packets and readable rooms reject encrypted packets. The relay does not add recording or plaintext logs.

One active voice session per account spans all rooms and approved devices. A move binds the destination room/device and waits for the existing four-second hard cutoff. Previous leases cannot renew during that fence. Concurrent moves cannot overlap, revive stale preparations or extend the shared generation deadline. Normal leave/logout/revocation also fences subsequent admission until old authority expires. Every move clears held PTT and requires a fresh press.

Nested channels (D-104)

Permission successor: SPEC-022/D-125 supersedes requireParentAccess, the additional parent-entry prerequisite and schema-8 access projection enforcement below with optional ACL inheritance in schema 9. The eight-level organizational hierarchy, hidden-ancestor projection, history boundaries and PTT rules remain. The following requireParentAccess paragraphs describe the schema-8 predecessor and its migration input; they must not override SPEC-022.

Ordinary channels have immutable parentId (null for a server-level channel, otherwise an existing ordinary channel at the same authority). Count the first channel below its server as depth one; maximum depth is eight. Text, voice and combined channels may each contain children. Derived thread calls and hidden direct/group conversations cannot be parents or children in this hierarchy. Preserve the existing 100-channel limit. channel-hierarchy/1 advertises this profile. Creation validates owner authority, parent identity/type and full acyclic depth inside the same transaction before creating any room or grants. Omission/null means server-level; malformed, missing, hidden-type or over-depth parents reject. Parent movement remains outside this increment; D-138 below adds recursive deletion.

requireParentAccess is a persisted channel administrative boolean, default true, supplied at creation or via revision-checked room.settings; omission on update retains the value, null/unknown types reject. On root channels it has no effect. For a child with true, the viewer must be enabled and have effective access to its parent, recursively, in addition to the child's own existing access/admission rules. Access to a parent does not require opening/admitting that parent. A false value explicitly starts an independent branch, preserving its own public/private access and grants; descendants may inherit that branch. Owner authority remains unchanged, and ordinary moderators gain no implicit private access.

Loss of required ancestor access closes affected descendant membership intervals at new room positions, removes preparations and live voice, and invalidates media authority through existing revision/lease enforcement. It does not remove the child's explicit grant; after access is restored, admission is explicit and absent-period history remains unavailable. Changing requireParentAccess to true applies the same revocation. Independent branches remain unaffected. Enforce this for API/history/device copies and direct media renewal, not only navigation. A hidden/inaccessible child remains indistinguishable from an unknown ID. The authorized metadata projection exposes only the nearest visible ancestor as parentId (or null), never hidden parent names/IDs; an accessible independent child remains reachable. Actual depth is projected as depth for creation limits without disclosing ancestor identities.

Schema 8 adds ordinary parent metadata and PTT mode overrides while preserving existing identifiers, messages, deadlines and membership. Legacy channels become server-level channels and inherit the mandatory server PTT default. Old schema-7 backends refuse schema 8, preventing older visibility rules from running against nested permissions. Restart is idempotent; no in-place downgrade.

Channel deletion and default selection (D-138)

CONFIRMED 2026-10-23: the server owner may delete any ordinary channel, including the initial Lobby, together with its entire ordinary subchannel tree. Derived thread calls belonging to those channels are removed too. Direct/group conversations are independent and cannot be deleted through this control. The client must display the channel and descendant names and explicitly warn that all their history and membership information will be permanently deleted and their voice/screen sessions ended. Cancel is the initial safe action; only an explicit Delete action submits the confirmed scope.

The authenticated owner obtains a preview from GET /api/server/channel-deletion?room=<id>. It returns the root, ordinary channel names/IDs, and a confirmation digest of their IDs, names and parent relationships. room.delete is a ticketed server-context command with exactly {target, confirmation}. The server recomputes that scope transactionally and rejects a changed tree with HTTP 409 so a fresh confirmation is required. Message/attendance changes do not invalidate the confirmation: the deletion applies to all data present at commit. Permissions are rechecked before mutation and result replay. Return only deleted IDs in the bounded idempotency receipt; reusing a ticket cannot recreate or repeat a deleted channel.

Remove channel rows, messages, membership intervals/access entries, channel invitations, encrypted history copies, channel screen policies, voice attendance and preparation. Revoke outstanding media authority under the existing hard bound; retain only bounded non-content command receipts and revocation work needed to prevent replay and finish enforcement. If the configured landing channel is deleted, clear that reference to null and advance the server-policy revision. Existing retained account identities, server membership and account invitations, other channels and independent direct conversations survive. Clients clear deleted-channel messages, drafts and local navigation/read state when deletion is observed. Previously exported or independently retained copies cannot be erased remotely, as already specified by SPEC-006. Restart never recreates a deleted Lobby.

/api/server/{state,sync,tickets,commands} provides authenticated server context independently of any channel. Its state has no room, no participants and no join capability; the owner can create a channel and all home accounts can edit their own profile. Other channel actions are rejected in that context. Tickets use the reserved @server scope, which is not a valid channel ID. New servers still start with the existing Lobby; older clients requesting a deleted channel receive not-found, never another channel's data. Advertise channel-deletion/1 for this additive protocol.

When a saved/default channel is missing or inaccessible, use the first accessible (admitted or canJoin) ordinary channel, sorted case-insensitively by name with ID as the stable tie-breaker. Explicit inaccessible link targets continue to report denial. If no channel is accessible, show server context and retain server settings and the owner's Create channel action. Selection opens the channel view without starting or moving voice.

Configuration

D-065 adds the non-configurable optional tree read projection in the D-065 section; default query value 0, allowed 0|1. Existing room/account limits bound the response; it adds no deployment or user preference.

The room creation fields, types/defaults/null semantics and writers are exactly those recorded in SPEC-001/003/004/008 and the accepted room review; no second defaults inventory is introduced here. The reviewed fixed maximum is 100 persistent rooms, including lobby. Existing ten members per room and ten simultaneous relay peers across the deployment remain. Room creation neither starts additional Containers nor changes the single shared generation/placement policy.

rooms/1 is a fixed capability string. Schema version 2, per-room counters, immutable room metadata, membership intervals, voice targets/fences and current conversation selection are operational state. The initial preferred conversation is the Lobby; D-138 selects an accessible saved/default channel or the alphabetically first accessible channel, with server context when none exist. D-138 supersedes the earlier exclusion of room deletion. No room-movement, access conversion, encryption conversion, federation or media-region switch is added in this increment. D-074 below supersedes the original exclusion of rename. Existing service-base configuration remains owned by SPEC-010.

Testing

D-138 units cover owner-only authorization, confirmation scope changes, recursive and Lobby deletion, message/history-copy/membership/invitation cleanup, bounded voice revocation, idempotent retries, restart without resurrection, unaffected sibling/direct data, empty-server management and fresh channel creation. Selection tests cover missing/inaccessible defaults, alphabetical order, ties and no accessible channels. Browser tests cover confirmation/cancel, visible consequences, fallback and empty-server creation.

D-104 units cover root creation, depths one/eight/nine, invalid/missing/hidden parents, independent overrides, parent denial despite child grants/admission, nested revocation and restored-access history gaps, non-leaking projection, roles, stale settings, atomic failure and schema-8 restart. Browser checks cover tree traversal/search, parent-named creation and both access modes.

D-095 tests cover schema-6 transactional PTT conversion and restart, preserved explicit limits, server/channel resolution, null reset, invalid/stale/unauthorized changes and revision propagation to thread calls.

Test the room-tree projection across multiple rooms: admitted, granted-but-unjoined, public-but-unjoined, inaccessible private, revoked membership, disabled account, expired/moved voice and invalid query values. Verify no projection changes admission or discloses keys, and omission preserves legacy sync shape.

Validate every creation field, default, explicit override, null/unknown value and limit; test owner/moderator/member rights and atomic rollback of invalid initial grants. Test schema 1 preservation and restart idempotence, independent room positions, private absence, public admission/denial, per-room history intervals, copied-history authorization and current role/device enforcement.

Test exact E2EE/readable variants, signed context and cross-room substitution, modality gates, scoped tickets/results, scoped preparations, independent text/voice selection and account-wide voice fences. Native relay tests verify cross-room/mode rejection, readable packet signature failures, legacy compatibility and unchanged lease/mute/restart bounds. Browser tests cover the shared creation/member screens and each preset with isolated test accounts. Real KDE/Windows input, human listening and international latency remain separate acceptance gates.

Acceptance criteria

  • D-222 raises existing shorter server and room durations to 600 seconds atomically once; inheritance and unrelated data survive, and restart preserves subsequent authorized shorter limits.
  • D-138: only the owner can confirm deletion; a changed descendant tree requires review again. All selected channels and associated stored history/membership are gone after commit and restart; independent channels/accounts remain.
  • Deleting the Lobby or last channel leaves working server management. Missing defaults select the alphabetically first accessible channel; an empty server offers owner creation without automatic voice.
  • D-104: create and navigate eight levels, reject a ninth atomically, and enforce required ancestor access across listing, text/history/copies and media; an explicit independent branch stays accessible without exposing hidden ancestors.
  • D-105: ptt-policy/1 exposes server requirement, nullable channel override and resolved pttRequired; explicit false overrides mandatory server policy, while conforming clients always preserve personal PTT.
  • D-095 server defaults and channel overrides resolve as SPEC-004 requires; schema-6 migration preserves non-60 limits and does not reconvert later explicit values on restart.
  • tree=1 returns current voice participants only in admitted rooms; hidden/unjoined rooms expose no roster, and expired/moved/revoked participants disappear on the next authorized read.
  • The owner creates each supported room preset with required details and receives an idempotent UUID; moderators and ordinary members cannot create rooms.
  • A granted server member sees and joins a private room without a code, while an ungranted account cannot list or probe its metadata/history.
  • Public/private defaults and explicit encryption choices are immutable and enforced for both supported modalities.
  • Existing lobby identities, tokens, keys, permissions, ciphertext and eligible history survive the transactional migration and restart unchanged.
  • Text, moderation, grants, membership and history remain isolated by room in UI and direct API tests; absent-period history never appears after a join.
  • Selecting another text conversation leaves voice in its named room; moving voice is explicit, fenced and requires fresh PTT.
  • E2EE/readable and cross-room media substitutions fail at the relay and client; the shared relay peer/generation limits remain unchanged.
  • Release evidence identifies implemented cases, tested migration/deployment order and remaining physical/platform checks without implying those checks passed.

D-063 batched synchronization

The sync/1 capability adds scoped/unscoped GET api/[rooms/<room>/]sync. The response contains the same authorized rooms, active voice, selected state, and eligible history as the existing reads, plus accounts only when explicitly requested and authorized. Query after is a nonnegative safe integer (default 0); historyRevision is an integer >= -1 (default -1); accounts is 0 or 1 (default 0). A differing history revision resets the effective cursor to 0. A non-admitted or voice-only selection includes no history. Recovery authority is the existing optional recovery query. Private visibility and administrator admission checks are identical to separate routes; no cross-room data is returned. Existing routes remain for compatibility. The client polls at the existing cadence, ignores stale account/channel results and fetches the roster only for open management. This is read batching, not a change to admission, history eligibility or media leases.

Room-tree navigation extension (D-065)

room-tree/1 is an additive service capability. GET /api/sync (including scoped variants) accepts tree=0|1, default 0; reject other values. With tree=1, each listed room adds voiceMembers only when the caller is currently admitted. Each entry contains only account id, username, domain, optional nickname, and room muted; it represents an enabled, admitted participant with an unexpired voice session for that room. Text-only admitted rooms return an empty list. Omit the field for visible but unjoined rooms; inaccessible private rooms remain absent. Do not return device keys, tokens, hidden room metadata or offline/member-directory data in this projection. The navigation list is not authorization for subsequent actions; context actions re-read scoped state and retain existing ticket/revision checks. No membership, history, media, polling cadence or rate-limit changes. Legacy clients omit tree and retain their existing response cost/shape; older servers without this capability use the selected room's authorized state only.

Channel renaming (D-074)

The room-rename/1 service capability permits the existing room.settings command to contain name, pttMaxSeconds, or both, with at least one field and no unknown fields. Name uses the same trimmed 1–80 Unicode-code-point, no-ASCII-control validation as creation; empty/null/non-string values fail. Existing owner/admitted-moderator permissions, room revision checks, idempotent ticket reconciliation and audit apply. Name-only changes also work in text-only rooms; a PTT field there still fails. Thread call settings remain inherited and cannot be edited independently.

This is an existing administrative name property, not a new deployment default. The stored name persists until an authorized update. Renaming changes no room ID, membership interval, message bytes, encryption, access or active-call identity. Derived thread call names use the new parent name. Client settings show an editable name and explicit Save name action when supported; failed saves retain the draft. Ordinary members see the current name read-only. Existing shared revision/PTT freshness rules still apply.

Testing extends room/service units for authorized rename, text-only naming, invalid bounds/types, denied ordinary members, stale revision and replay; verify history, IDs and membership survive. Browser acceptance checks the settings form, channel tree, header and connected call label after a rename and after reload.

Server and channel PTT duration inheritance (D-095)

SPEC-004 owns duration values, validation and inheritance. ptt-inheritance/1 advertises nullable channel pttMaxSeconds input and response fields pttMaxSecondsOverride and serverPttMaxSeconds, while pttMaxSeconds remains the resolved numeric limit for older clients. For duration, room.settings accepts null as inheritance; D-105 adds a nullable mode override below. Text-only rooms still reject a PTT setting. Thread calls expose their parent's resolved policy and cannot be edited independently.

Schema 6 adds explicit override metadata transactionally. For each legacy room, 60 seconds becomes null (inherit) and any other valid duration is preserved as an override. Existing explicit metadata is preserved. The current server default is 600 seconds under D-222. Account/device identities, messages, membership, sessions and room IDs remain intact. A changed effective value increments the room revision, and restart cannot repeat conversion of a newly explicit 60-second override. Old schema-5 backends refuse schema 6; use forward repair, not in-place downgrade.

Ten-minute duration upgrade (D-222)

After existing schema migrations, one transaction raises every saved server duration and every room's effective duration to 600 seconds. Preserve null channel inheritance; raise explicit overrides to 600 seconds, including hidden conversation and thread records. Increment affected server/room revisions, preserving every unrelated policy field, identity, membership, message, session and counter. Store meta/ptt-duration-600 as boolean true in the same transaction. This fixed internal migration marker is operational state, not a setting. It is initially absent; after success it prevents restart from replacing subsequently authorized shorter limits. Failure rolls back all duration writes and the marker. No table or wire shape changes; schema remains 9. Earlier backends validate only up to 300 seconds and are incompatible with upgraded policy: use forward repair rather than rollback.

Unit tests cover mixed inherited/explicit/hidden rooms, exact 600-second values, revision changes, unrelated-field preservation, atomic rollback, restart and subsequent shorter settings.

PTT mode protocol (D-105)

ptt-policy/1 advertises server boolean pttRequired and nullable channel pttRequired creation/settings input. Read metadata exposes pttRequiredOverride, serverPttRequired and resolved pttRequired; legacy clients still use mandatory PTT. Text-only channels reject PTT mode fields. A thread call inherits its immediate conversation parent's resolved policy; DM/temporary calls inherit their host's server policy. Ordinary nested channels inherit the server directly, not their organizational parent. SPEC-004 owns defaults and effective personal mode. Changing effective mode increments room revisions and clears live transmission using existing enforcement, including thread calls. No physical-input attestation is claimed.