Persistence-spec.md

Markdown Detected Guest 7 Views Size: 33.48 KB Posted on: Sep 4, 26 @ 6:32 AM
  1. ---
  2. title: "`draft/persistence` Extension"
  3. layout: spec
  4. work-in-progress: true
  5. copyrights:
  6.   -
  7.     name: "MrLenin"
  8.     period: "2026"
  9. ---
  10.  
  11. ## Notes for implementing work-in-progress version
  12.  
  13. This is a work-in-progress specification.
  14.  
  15. Software implementing this work-in-progress specification MUST NOT use the unprefixed `persistence` CAP name, command name, or batch type.  Implementations SHOULD use the `draft/persistence` prefix on the CAP and batch type, and the literal `PERSISTENCE` command name, to be interoperable with other software implementing a compatible work-in-progress version.  The final version of the specification will use the unprefixed `persistence` CAP name and batch type.
  16.  
  17. This specification supersedes the in-house `BOUNCER` command surface previously used by Nefarious for the same purpose.  The `BOUNCER` command is retained for one release cycle for tooling compatibility and is expected to be removed in a future major version.
  18.  
  19. ## Introduction
  20.  
  21. This specification describes a mechanism for IRC clients and servers to negotiate persistent sessions — connections whose state (nick, channel memberships, away status, preferences, and related metadata) is retained by the server across client disconnections.
  22.  
  23. When the client reconnects, the server restores that state to the new connection in a way the client can distinguish from live network activity.  When a second client connects to the same account, the server attaches it as an additional view onto the same session.
  24.  
  25. The specification covers:
  26.  
  27.   1. Discovery of persistence support via a CAP.
  28.   2. A `PERSISTENCE` command for the client to inspect and adjust its session, organise per-connection configuration as named profiles, attach to a chosen profile at registration time, control auto-replay of missed messages, and detach from persistence entirely.
  29.   3. A `draft/persistence` batch type wrapping the server's channel-state restoration burst.
  30.   4. A `evilnet.github.io/bouncer-replay` batch type wrapping the server's optional missed-message replay.
  31.   5. A reserved metadata key prefix (`draft/persistence/`) for server-managed state related to this specification.
  32.   6. A network-membership reconciliation mechanism for clients that participate in profile-driven divergent channel sets across multiple concurrent connections.
  33.  
  34. The server as mentioned in this document MAY be an IRC server or an IRC bouncer.
  35.  
  36. ## Motivation
  37.  
  38. Persistent connections are a longstanding IRC pattern implemented by bouncers (e.g. ZNC, soju) and native server features (e.g. Ergo, Nefarious).  Despite widespread deployment, there is no standard protocol for:
  39.  
  40.   1. **Discovery** — Clients cannot reliably distinguish a persistent server from a stateless one, leading to harmful client-driven auto-rejoin on reconnect.
  41.   2. **State restoration boundary** — When the server replays channel state, clients cannot distinguish replayed joins from live activity, and lack a signal that the replay is complete.
  42.   3. **Per-connection configuration** — Users with multiple concurrent clients (e.g. mobile, desktop) cannot easily express that different clients should observe different subsets of their session state.
  43.   4. **Lifecycle control** — Clients cannot request or query persistence status, opt out at runtime, or pick among multiple held sessions.
  44.  
  45. This specification addresses each of these and is structured so that conforming servers may implement progressively richer subsets independently.
  46.  
  47. ## Architecture
  48.  
  49. ### Dependencies
  50.  
  51. Negotiating the `batch` ([`batch`][batch]) and `message-tags` ([`message-tags`][message-tags]) capabilities is RECOMMENDED for full functionality.  The `draft/persistence` batch types defined below require the `batch` capability; when `batch` is not negotiated the server falls back to sending state restoration messages without the batch wrapper.
  52.  
  53. Negotiating `draft/metadata-2` ([`draft/metadata-2`][metadata2]) is RECOMMENDED.  This specification reserves a server-managed metadata key prefix; clients may read those keys via `METADATA GET` even when not setting them.
  54.  
  55. ### Capability
  56.  
  57. This specification adds the `draft/persistence` capability.
  58.  
  59. Clients requesting this capability indicate that they:
  60.  
  61.   - understand the `PERSISTENCE` command and its replies as defined below, and
  62.   - will refrain from sending JOIN commands for channels they intend to be restored from a persistent session (auto-rejoin suppression — see [Client behaviour](#client-behaviour)).
  63.  
  64. The capability MAY carry a comma-separated value indicating the OPTIONAL subcommands the server supports beyond the base `STATUS`/`GET`/`SET` triad:
  65.  
  66. ```
  67. CAP * LS :draft/persistence=replay-control,profile,list,attach
  68. ```
  69.  
  70. Defined optional tokens:
  71.  
  72. | Token            | Meaning                                                       |
  73. |------------------|---------------------------------------------------------------|
  74. | `replay-control` | Server supports `PERSISTENCE REPLAY` subcommands              |
  75. | `profile`        | Server supports `PERSISTENCE PROFILE` subcommands             |
  76. | `attach`         | Server supports `PERSISTENCE ATTACH` (and the `PROFILE` model)|
  77. | `detach`         | Server supports `PERSISTENCE DETACH`                          |
  78. | `list`           | Server supports `PERSISTENCE LIST` or `PERSISTENCE PROFILE LIST` |
  79. | `attach-cursor`  | `PERSISTENCE ATTACH <profile> [<msgid>]` — optional last-seen msgid anchors the server-driven catch-up replay (unknown msgid → `FAIL PERSISTENCE CURSOR_UNKNOWN` + fallback to last-activity). Supplying the cursor is consent to **unsolicited `chathistory`-type batches** (standard `CHATHISTORY AFTER` response shape, no command issued), wrapped in an outer `evilnet.github.io/bouncer-replay` batch when `batch` is negotiated: channel batches first (read-marker-advanced), then PM correspondents; per-buffer cap default 100 with newest-biased truncation (gap sits after the cursor — detect by msgid discontinuity, backfill with `CHATHISTORY AFTER`); may interleave with live traffic at batch boundaries — order by `server-time`, dedup by `msgid`. Applies to `draft/chathistory`-capable clients only when a cursor is supplied; gated by server auto-replay policy and the user's `PERSISTENCE REPLAY` setting |
  80.  
  81. Clients MUST tolerate unknown tokens in the value and MUST NOT assume the absence of a token implies the absence of a feature; the value is a hint, not an authoritative inventory.
  82.  
  83. ### Server-managed metadata keys
  84.  
  85. This specification stores per-account configuration as metadata keys under the prefix:
  86.  
  87.   - `draft/persistence/` while this document remains a work-in-progress draft.
  88.   - `persistence/` once this document is ratified.
  89.  
  90. These keys are *server-managed*: their values are maintained by the server in response to the `PERSISTENCE` command defined below, and direct `METADATA SET` from a client is refused.
  91.  
  92. When the `draft/metadata-2` capability is in use, servers MUST refuse client-initiated `METADATA SET` against any key that begins with this specification's prefix, replying with:
  93.  
  94. ```
  95. FAIL METADATA KEY_NO_PERMISSION <target> <key> :Key is server-managed and cannot be set directly
  96. ```
  97.  
  98. Server-managed keys MUST NOT count against the per-target `MAX_KEYS` budget advertised by the `draft/metadata-2` capability.  Servers MAY allow privileged out-of-band paths (e.g. an oper-only administrative interface keyed on `*<account>`) to write to these keys; this is implementation-defined and not part of the wire protocol.
  99.  
  100. Server-managed keys remain readable via `METADATA GET`, so clients MAY inspect them.
  101.  
  102. Specifications and vendor extensions that introduce additional server-managed keys SHOULD use their own draft-namespaced or vendor-scoped prefix (e.g. `draft/my-extension/...` or `vendor.example/...`) rather than claiming generic unprefixed prefixes.  This avoids namespace conflicts and keeps the carve-out auditable per-specification.
  103.  
  104. ## Commands
  105.  
  106. The `PERSISTENCE` command introduced by this specification has the syntax:
  107.  
  108. ```
  109. PERSISTENCE <subcommand> [<argument> ...]
  110. ```
  111.  
  112. Subcommand names defined here are case-insensitive; clients SHOULD send them in upper case.  Servers MUST accept any case.
  113.  
  114. The `draft/persistence` capability is a client opt-in signal: it advertises to the server that the client understands the unsolicited `PERSISTENCE STATUS` line at registration and the *semantics* of the `draft/persistence`-typed batch (channel-state restoration vs. live activity).
  115.  
  116. Servers MUST NOT refuse `PERSISTENCE` commands from clients that have not negotiated the capability — the command surface remains available to any authenticated client.  Auto-replay and other server-side effects defined by this specification likewise apply regardless of capability negotiation.
  117.  
  118. Capability gating in this specification scopes:
  119.  
  120.   - The registration-time unsolicited `PERSISTENCE STATUS` — gated on `draft/persistence`.
  121.   - The `draft/persistence` and `evilnet.github.io/bouncer-replay` batch *envelopes* — gated on `batch` only.  Per the `batch` extension, unknown batch types are tolerated, so a client that has `batch` without `draft/persistence` still receives the envelope and benefits from the grouping signal.  The `draft/persistence` capability adds *interpretation* (treat the batch contents as restoration, suppress join notifications, etc.); it is not a precondition for emitting the envelope.
  122.  
  123. Authentication is required for every subcommand defined in this specification.  When the calling connection has not authenticated, the server MUST reply:
  124.  
  125. ```
  126. FAIL PERSISTENCE ACCOUNT_REQUIRED <subcommand> :You must be authenticated
  127. ```
  128.  
  129. Unknown subcommands and ill-formed arguments produce:
  130.  
  131. ```
  132. FAIL PERSISTENCE INVALID_PARAMETERS <context> :<description>
  133. ```
  134.  
  135. where `<context>` identifies the offending subcommand or argument and `<description>` is human-readable.
  136.  
  137. ### STATUS (base, REQUIRED)
  138.  
  139. ```
  140. PERSISTENCE STATUS                             ; client to server
  141. :server PERSISTENCE STATUS <client-setting> <effective-setting>  ; server to client
  142. ```
  143.  
  144. `<client-setting>` is one of `ON`, `OFF`, or `DEFAULT` and reflects what the client has explicitly requested (`DEFAULT` when no explicit preference is stored).  `<effective-setting>` is one of `ON` or `OFF` and reflects the effective persistence state for the calling connection.  (This matches PR #503; an earlier revision of this document described a one-argument form, which the implementation followed until 2026-08-28 — both are now on the two-argument form.)
  145.  
  146. The server MUST send an unsolicited `PERSISTENCE STATUS` to a connection that has negotiated `draft/persistence` and is authenticated.  This unsolicited message MUST be sent after the final `005` (`RPL_ISUPPORT`) and before `376` (`RPL_ENDOFMOTD`) or `422` (`ERR_NOMOTD`).
  147.  
  148. Servers MUST also send `PERSISTENCE STATUS` as the reply to `PERSISTENCE GET` and after every successful `PERSISTENCE SET` and `PERSISTENCE ATTACH`.
  149.  
  150. ### GET (base, REQUIRED)
  151.  
  152. ```
  153. PERSISTENCE GET                                ; client to server
  154. ```
  155.  
  156. The server replies with `PERSISTENCE STATUS` reflecting the current effective state.
  157.  
  158. ### SET (base, REQUIRED)
  159.  
  160. ```
  161. PERSISTENCE SET <argument>                     ; client to server
  162. ```
  163.  
  164. `<argument>` is one of `ON`, `OFF`, or `DEFAULT`.
  165.  
  166.   - `ON` — Request that persistence be enabled for the account.  The server MUST record this preference at account scope; if no session exists, the server SHOULD create one.
  167.   - `OFF` — Request that persistence be disabled for the account.  The server MUST record this preference at account scope; if a session exists, the server SHOULD destroy it (see [DETACH](#detach-optional) for the multi-connection case).
  168.   - `DEFAULT` — Clear any account-scope preference; the effective state then falls back to the server's default policy.
  169.  
  170. The server MUST reply with both:
  171.  
  172. ```
  173. :server PERSISTENCE SET <argument>
  174. :server PERSISTENCE STATUS <client-setting> <effective-setting>
  175. ```
  176.  
  177. The reply MAY appear in either order; the `STATUS` line carries the stored client setting (which after a successful SET equals `<argument>`) and the new effective state, which need not match it (e.g. when server policy enforces `ON`).
  178.  
  179. ### REPLAY (REQUIRED if `replay-control` advertised)
  180.  
  181. This subcommand controls the optional missed-message replay (see [`evilnet.github.io/bouncer-replay` batch](#evilnetgithubiobouncer-replay-batch)).  It does not affect the channel-state restoration burst (see [`draft/persistence` batch](#draftpersistence-batch)) — joining, topic, and member-list restoration are part of the session's current state, not historical activity.
  182.  
  183. ```
  184. PERSISTENCE REPLAY GET                         ; client to server
  185. PERSISTENCE REPLAY SET <argument>              ; client to server
  186. :server PERSISTENCE REPLAY STATUS <client-setting> <effective>
  187. ```
  188.  
  189. `<argument>` is one of `ON`, `OFF`, or `DEFAULT`.  `<client-setting>` is `ON`, `OFF`, or `DEFAULT` and reports the user's explicit preference.  `<effective>` is `ON` or `OFF` and reports what the server actually does for this connection.
  190.  
  191. `GET` returns the current `REPLAY STATUS`.  `SET <argument>` updates the preference, replies with an acknowledgement followed by `REPLAY STATUS`:
  192.  
  193. ```
  194. :server PERSISTENCE REPLAY SET <argument>
  195. :server PERSISTENCE REPLAY STATUS <client-setting> <effective>
  196. ```
  197.  
  198. ### PROFILE (REQUIRED if `profile` advertised)
  199.  
  200. A *profile* is a named bundle of per-connection configuration for the calling account.  Every account has an implicit `default` profile that is always present and cannot be removed.
  201.  
  202. ```
  203. PERSISTENCE PROFILE LIST
  204. PERSISTENCE PROFILE CREATE <name> [FROM <parent>]
  205. PERSISTENCE PROFILE DELETE <name>
  206. PERSISTENCE PROFILE RENAME <old-name> <new-name>
  207. PERSISTENCE PROFILE GET <name> <key>
  208. PERSISTENCE PROFILE SET <name> <key> <value>
  209. PERSISTENCE PROFILE SET <name> <key> DEFAULT
  210. ```
  211.  
  212. #### Profile names
  213.  
  214. A profile name MUST consist of one to 32 ASCII characters drawn from `A-Z`, `a-z`, `0-9`, `_`, and `-`.  Profile names are case-insensitive for matching.  The literal name `default` is reserved for the implicit default profile and MUST NOT be the target of `CREATE`, `DELETE`, or `RENAME`.
  215.  
  216. #### Profile inheritance
  217.  
  218. `CREATE <name> FROM <parent>` makes `<name>` inherit from `<parent>`.  `CREATE <name>` without a `FROM` clause is equivalent to `CREATE <name> FROM default`.  Cycles MUST be refused at `CREATE` and `SET` time.
  219.  
  220. A profile's *effective* value for a given key is resolved by walking from the profile root-ward through parents and applying overrides leaf-ward.  For scalar keys, the first explicit setting closest to the leaf wins.  For the channel-list key (defined below), the resolution composes adds and subtracts (see [Channel lists](#channel-lists)).
  221.  
  222. The server MUST refuse `DELETE` of a profile that is an ancestor of any other existing profile, replying with `FAIL PERSISTENCE INVALID_PARAMETERS`.
  223.  
  224. #### Profile keys
  225.  
  226. The following keys are defined by this specification for use under `PERSISTENCE PROFILE SET <name> <key> <value>`.
  227.  
  228. | Key            | Type       | Meaning                                                  |
  229. |----------------|------------|----------------------------------------------------------|
  230. | `parent`       | name       | Parent profile in the inheritance chain                  |
  231. | `hold`         | `0`/`1`    | Per-profile persistence preference                       |
  232. | `auto-replay`  | `0`/`1`    | Per-profile missed-message replay preference             |
  233. | `channels`     | set ops    | Channel-list edits (see below)                           |
  234.  
  235. The `channels` key uses set operations:
  236.  
  237. ```
  238. PERSISTENCE PROFILE SET <name> channels +#x   ; add #x
  239. PERSISTENCE PROFILE SET <name> channels -#x   ; remove #x (or mark inherited #x as subtracted)
  240. PERSISTENCE PROFILE SET <name> channels DEFAULT
  241. ```
  242.  
  243. `DEFAULT` clears the profile's own contribution to its channel list (the effective list then equals the parent's effective list).
  244.  
  245. Future revisions or vendor extensions MAY define additional keys.  Servers MUST reject `SET` on keys they do not recognise with `FAIL PERSISTENCE INVALID_PARAMETERS`.
  246.  
  247. #### LIST output
  248.  
  249. ```
  250. :server PERSISTENCE PROFILE <name> [<key>=<value> ...]
  251. :server PERSISTENCE PROFILE ENDOFLIST
  252. ```
  253.  
  254. The server MUST emit one `PROFILE` line per profile defined for the account, in arbitrary order, terminated by `ENDOFLIST`.  Lines MAY include trailing `<key>=<value>` pairs reporting profile attributes (notably `parent=<name>`); clients MUST tolerate unknown attributes.
  255.  
  256. The implicit `default` profile is always reported, even if no keys are set on it.
  257.  
  258. #### GET reply
  259.  
  260. ```
  261. :server PERSISTENCE PROFILE <name> <key> :<value>
  262. :server PERSISTENCE PROFILE <name> <key>
  263. ```
  264.  
  265. The first form indicates the key has an effective value; the second indicates the key is unset.  For the `channels` key, the value is the effective channel list as a comma-separated string with inheritance applied.
  266.  
  267. #### SET acknowledgement
  268.  
  269. ```
  270. :server PERSISTENCE PROFILE <name> <key> :<value>
  271. :server PERSISTENCE PROFILE <name> <key>          ; key cleared
  272. ```
  273.  
  274. #### CREATE / DELETE / RENAME acknowledgement
  275.  
  276. ```
  277. :server PERSISTENCE PROFILE CREATED <name> parent=<parent>
  278. :server PERSISTENCE PROFILE DELETED <name>
  279. :server PERSISTENCE PROFILE RENAMED <old-name> <new-name>
  280. ```
  281.  
  282. ### ATTACH (REQUIRED if `attach` advertised)
  283.  
  284. ```
  285. PERSISTENCE ATTACH <profile>                   ; client to server
  286. :server PERSISTENCE ATTACH <profile>
  287. ```
  288.  
  289. `ATTACH` selects the active profile for the calling connection.  The server MUST accept `ATTACH` only between SASL completion and the end of capability negotiation (i.e. before `CAP END`).  After registration, the server MUST refuse with `FAIL PERSISTENCE INVALID_PARAMETERS`.
  290.  
  291. The active profile drives:
  292.  
  293.   - delivery filtering against the profile's effective `channels` list,
  294.   - the resolution chain for `STATUS`, `REPLAY STATUS`, and other per-connection settings,
  295.   - the contents of the channel-state restoration burst.
  296.  
  297. If the client does not send `ATTACH` before registration completes, the active profile is `default`.
  298.  
  299. ### DETACH (REQUIRED if `detach` advertised)
  300.  
  301. ```
  302. PERSISTENCE DETACH [<session-id>]              ; client to server
  303. :server PERSISTENCE DETACH OK | NOSESSION
  304. ```
  305.  
  306. `DETACH` without a `<session-id>` argument requests that the caller's current session be released.  Released means: any other connections attached to the session are disconnected, the held/ghost state is cleared, account-scope `hold` is set to `OFF`, and the caller continues as a normal non-persistent client.
  307.  
  308. If the caller has no active session, the server replies `:server PERSISTENCE DETACH NOSESSION`; this is informational, not an error.
  309.  
  310. If the calling session is marked as enforced by server policy (e.g. a connection class with an enforced-persistence flag is currently attached), the server MUST refuse:
  311.  
  312. ```
  313. FAIL PERSISTENCE CANNOT_DETACH :Connection class enforces persistence; cannot detach
  314. ```
  315.  
  316. The session-enforced flag MUST be cleared when the session transitions to fully-held (no live connections); a subsequent non-enforced reattach may then `DETACH` normally.
  317.  
  318. If `<session-id>` refers to a different session owned by the same account, the server MUST destroy that session without affecting the caller.
  319.  
  320. ## Batch types
  321.  
  322. ### `draft/persistence` batch
  323.  
  324. When a client with the `batch` capability resumes a session — whether by reconnecting to a held session or by attaching as an additional view onto an active session — the server MUST wrap the resulting channel-state restoration in a batch of type `draft/persistence`:
  325.  
  326. ```
  327. :server BATCH +<ref> draft/persistence
  328. :nick!user@host JOIN #channel
  329. :server 332 nick #channel :Topic text
  330. :server 333 nick #channel setter 1713234120
  331. :server 353 nick = #channel :@op +voice nick
  332. :server 366 nick #channel :End of /NAMES list.
  333. :nick!user@host JOIN #another
  334. ...
  335. :server BATCH -<ref>
  336. ```
  337.  
  338. The batch's contents represent the *current* state of the connection's view, not historical activity.  Clients that have negotiated `draft/persistence` MUST NOT treat these joins, mode messages, or topic messages as new events for the purpose of notifications, auto-join scripts, or analogous side effects.  Clients that have `batch` but not `draft/persistence` MAY treat the batch contents as live activity (the spec extends them no semantic obligation beyond standard `batch` behaviour), but SHOULD use the grouping signal where useful.
  339.  
  340. The batch boundary MUST encompass all channel-state restoration triggered by the resume.  After `BATCH -<ref>`, any subsequent JOINs, modes, or topics are live activity.
  341.  
  342. When the client has not negotiated `batch`, the server MUST still send the restoration burst but does so without the wrapping `BATCH` lines.  Clients SHOULD treat all JOINs received between the welcome (`001`) and the MOTD-end (`376`/`422`) as restoration; subsequent JOINs are live activity.
  343.  
  344. #### Empty restoration
  345.  
  346. If the resuming connection has no channels in its effective view, the server MAY omit the `draft/persistence` batch entirely.  The client MUST tolerate either behaviour.
  347.  
  348. ### `evilnet.github.io/bouncer-replay` batch
  349.  
  350. When the server replays missed messages on resume (subject to the resolved `auto-replay` preference and the absence of `draft/chathistory` on the client), the replay MUST be wrapped in a vendor-scoped batch of type `evilnet.github.io/bouncer-replay`:
  351.  
  352. ```
  353. :server BATCH +<outer> evilnet.github.io/bouncer-replay
  354. @batch=<outer> :server BATCH +<inner> chathistory #channel
  355. @batch=<inner> :nick!user@host PRIVMSG #channel :live message before the gap
  356. @batch=<inner> :nick!user@host PRIVMSG #channel :and another
  357. @batch=<outer> :server BATCH -<inner>
  358. :server BATCH -<outer>
  359. ```
  360.  
  361. The outer batch boundary signals the entire missed-message replay block, useful for unread-marker rollup, do-not-disturb suppression, and notification batching.
  362.  
  363. Servers MUST emit the outer batch lazily — on the first inner `chathistory` batch — so that empty replays do not ship an empty wrapper.
  364.  
  365. Servers MUST NOT emit this batch when the client has negotiated `draft/chathistory`; such clients fetch history themselves.
  366.  
  367. This batch type is vendor-scoped on `evilnet.github.io` because the host is DNS-resolvable and demonstrably controlled by the originating organisation; the namespace MAY be promoted to a non-vendor draft in a future revision.
  368.  
  369. ## Channel lists
  370.  
  371. Each profile has an *effective channel list*, computed by walking the inheritance chain root-to-leaf and applying each profile's own contribution:
  372.  
  373.   - A `<channel>` entry adds the channel to the running set.
  374.   - A `-<channel>` entry removes the channel from the running set, regardless of whether it was inherited.
  375.  
  376. `PERSISTENCE PROFILE SET <name> channels +<channel>` ensures the channel is present in the effective list at `<name>`:
  377.  
  378.   - If the inheritance chain already contains `<channel>` above `<name>`, the server MUST remove any local `-<channel>` subtract entry and otherwise leave the profile's own list untouched.
  379.   - Otherwise the server MUST add `<channel>` to the profile's own list.
  380.  
  381. `PERSISTENCE PROFILE SET <name> channels -<channel>` ensures the channel is absent from the effective list at `<name>`:
  382.  
  383.   - If the inheritance chain contains `<channel>` above `<name>`, the server MUST add `-<channel>` to the profile's own list.
  384.   - Otherwise the server MUST remove any local `<channel>` entry.
  385.  
  386. An *empty effective channel list* is the permissive default and means "no filter": the connection observes traffic for all channels the account is currently in.  A non-empty effective channel list means "filter to these channels": the connection observes traffic only for channels in the effective list.
  387.  
  388. When a connection's active profile has a non-empty effective channel list, the server MUST filter every channel-routed delivery to that connection (PRIVMSG, NOTICE, TAGMSG, JOIN, PART, MODE, TOPIC, …) by membership in the effective list.
  389.  
  390. When the connection issues `/JOIN <channel>` while filtering is active (effective list non-empty), the server MUST update the active profile's channel list as if `PROFILE SET <profile> channels +<channel>` had been issued, before performing the network join.
  391.  
  392. When the connection issues `/PART <channel>` while filtering is active, the server MUST update the active profile's channel list as if `PROFILE SET <profile> channels -<channel>` had been issued, after performing the network part.
  393.  
  394. The default profile's initial channel list is empty, preserving the "no filter, mirror primary" behaviour for legacy clients.
  395.  
  396. ## Network-membership reconciliation
  397.  
  398. This section is OPTIONAL.  Servers MAY implement it when the account-scope `hold` setting resolves to `ON` and the user has multiple concurrent connections with divergent profile channel lists.
  399.  
  400. For accounts whose effective `hold` is `OFF`, the server SHOULD treat profiles as advisory and apply only the per-delivery filter described in [Channel lists](#channel-lists).  Profile-driven divergent network presence is undefined for non-persistent accounts.
  401.  
  402. For accounts whose effective `hold` is `ON`, the server MAY model concurrent connections as separate channel members with mirror memberships under the account's network identity.  In this model:
  403.  
  404.   - The account has a single primary network presence (the connection currently selected as the session's primary).
  405.   - Each additional concurrent connection is an *alias* with its own per-channel mirror memberships.
  406.   - When a connection issues `/PART <channel>`, the server consults the union over all profiles' effective channel lists for the account.  If any other profile still wants the channel, the server MUST suppress the network-level part for the calling connection and emit a synthetic PART to the connection's own view only.  If no other profile wants the channel, the server proceeds with a normal network part, which removes mirror memberships across all connections of the account.
  407.   - When a connection issues `/JOIN <channel>` for a channel that is already a network member of the account (via another connection's profile), the server MUST emit a synthetic JOIN echo to the joining connection along with a single-channel state burst (`TOPIC`, `RPL_TOPICWHOTIME`, `NAMES`) reflecting the channel's current state.
  408.  
  409. The propagation mechanism for cross-server mirror memberships is implementation-defined and outside the scope of this specification.
  410.  
  411. ## Client behaviour
  412.  
  413. ### Auto-rejoin suppression
  414.  
  415. A client that has negotiated `draft/persistence` MUST NOT send JOIN commands for channels it remembers from a previous connection.  The client SHOULD instead wait for the channel-state restoration burst.  The client MAY send JOIN for channels not received from the server after the restoration boundary.
  416.  
  417. This rule applies regardless of the effective `STATUS` value: even when `STATUS` reports `OFF`, the presence of the `draft/persistence` capability is sufficient grounds to suppress client-driven auto-rejoin and to instead respect whatever the server delivers.
  418.  
  419. ### Reading STATUS
  420.  
  421. A client SHOULD NOT alter its UI based solely on the `<state>` value of `PERSISTENCE STATUS`.  The status is informational; clients that want to expose persistence state to users SHOULD use a wording that reflects "the server is holding my session" rather than "I am persistent".
  422.  
  423. ### Profile selection at registration
  424.  
  425. A client that wants to attach to a specific profile MUST do so between SASL completion and `CAP END`:
  426.  
  427. ```
  428. C: CAP LS 302
  429. S: CAP * LS :draft/persistence=profile,attach sasl batch
  430. C: CAP REQ :draft/persistence batch sasl
  431. S: CAP * ACK :draft/persistence batch sasl
  432. C: AUTHENTICATE PLAIN
  433. ... SASL exchange ...
  434. S: 900 * account :You are now logged in
  435. C: PERSISTENCE PROFILE LIST
  436. S: :server PERSISTENCE PROFILE default
  437. S: :server PERSISTENCE PROFILE mobile parent=default
  438. S: :server PERSISTENCE PROFILE ENDOFLIST
  439. C: PERSISTENCE ATTACH mobile
  440. S: :server PERSISTENCE ATTACH mobile
  441. C: CAP END
  442. ```
  443.  
  444. If the client does not send `ATTACH`, the server's auto-selection logic applies (the active profile defaults to `default`).
  445.  
  446. ## Errors
  447.  
  448. The following error code values are defined for `FAIL PERSISTENCE`:
  449.  
  450. | Code                  | Meaning                                                        |
  451. |-----------------------|----------------------------------------------------------------|
  452. | `ACCOUNT_REQUIRED`    | Authentication is required to use this subcommand              |
  453. | `INVALID_PARAMETERS`  | Subcommand arguments were malformed or referred to non-existent names |
  454. | `INTERNAL_ERROR`      | Server failed to process the request                           |
  455. | `CANNOT_DETACH`       | The session is class-enforced and cannot be detached           |
  456. | `NO_SUCH_SESSION`     | The session id does not exist or is not owned by the account   |
  457.  
  458. Servers SHOULD use the `<context>` field of `FAIL` to indicate the offending subcommand and any relevant argument.
  459.  
  460. ## Security considerations
  461.  
  462.   - Persistence carries elevated risk of resource exhaustion.  Servers SHOULD limit the number of concurrent held sessions per account, enforce inactivity timeouts for held sessions, and require SASL authentication before creating a session.
  463.   - The per-account profile space (`draft/persistence/profile/...`) is metadata: it is replicated by the metadata distribution mechanism to peers that share the account.  Implementations MUST treat these keys as PRIVATE (visible only to the owning account) for purposes of `draft/metadata-2`'s visibility model.
  464.   - The optional network-membership reconciliation creates a divergence between an account's per-channel network presence and the per-connection view of that presence.  Operators SHOULD audit channel-routed administrative tooling (KICK, BAN, MODE delivery) to confirm it behaves predictably under this divergence.
  465.   - `PERSISTENCE LIST` and the listing of profile names can reveal that an account has multiple held sessions or multiple configured profiles.  Servers MUST only return data owned by the authenticated account.
  466.  
  467. ## Examples
  468.  
  469. ### Minimal flow
  470.  
  471. ```
  472. C: CAP LS 302
  473. S: CAP * LS :draft/persistence batch sasl
  474. C: CAP REQ :draft/persistence batch sasl
  475. S: CAP * ACK :draft/persistence batch sasl
  476. C: AUTHENTICATE PLAIN
  477. ... SASL exchange ...
  478. C: CAP END
  479. S: ... registration burst ...
  480. S: 005 nick ... :are supported by this server
  481. S: :server PERSISTENCE STATUS ON
  482. S: :server BATCH +1 draft/persistence
  483. S: :nick!user@host JOIN #chan
  484. S: :server 332 nick #chan :Topic
  485. S: :server 366 nick #chan :End of /NAMES
  486. S: :server BATCH -1
  487. S: 376 nick :End of /MOTD
  488. ```
  489.  
  490. ### Profile + ATTACH
  491.  
  492. ```
  493. C: PERSISTENCE PROFILE LIST
  494. S: :server PERSISTENCE PROFILE default
  495. S: :server PERSISTENCE PROFILE mobile parent=default
  496. S: :server PERSISTENCE PROFILE ENDOFLIST
  497. C: PERSISTENCE ATTACH mobile
  498. S: :server PERSISTENCE ATTACH mobile
  499. C: CAP END
  500. S: ... registration burst ...
  501. S: :server PERSISTENCE STATUS ON
  502. ```
  503.  
  504. ### Editing a profile's channels
  505.  
  506. ```
  507. C: PERSISTENCE PROFILE CREATE mobile FROM default
  508. S: :server PERSISTENCE PROFILE CREATED mobile parent=default
  509. C: PERSISTENCE PROFILE SET mobile channels +#urgent
  510. S: :server PERSISTENCE PROFILE mobile channels :#urgent
  511. C: PERSISTENCE PROFILE SET mobile channels +#priority
  512. S: :server PERSISTENCE PROFILE mobile channels :#urgent,#priority
  513. C: PERSISTENCE PROFILE GET mobile channels
  514. S: :server PERSISTENCE PROFILE mobile channels :#urgent,#priority
  515. ```
  516.  
  517. ### REPLAY control
  518.  
  519. ```
  520. C: PERSISTENCE REPLAY GET
  521. S: :server PERSISTENCE REPLAY STATUS DEFAULT ON
  522. C: PERSISTENCE REPLAY SET OFF
  523. S: :server PERSISTENCE REPLAY SET OFF
  524. S: :server PERSISTENCE REPLAY STATUS OFF OFF
  525. ```
  526.  
  527. ### DETACH
  528.  
  529. ```
  530. C: PERSISTENCE DETACH
  531. S: :server PERSISTENCE DETACH OK
  532. S: :server PERSISTENCE STATUS OFF
  533. ```
  534.  
  535. If class-enforced:
  536.  
  537. ```
  538. C: PERSISTENCE DETACH
  539. S: FAIL PERSISTENCE CANNOT_DETACH DETACH :Connection class enforces persistence; cannot detach
  540. ```
  541.  
  542. ### Reading server-managed metadata
  543.  
  544. ```
  545. C: METADATA * GET draft/persistence/hold
  546. S: :server 761 nick * draft/persistence/hold private :1
  547. C: METADATA * SET draft/persistence/hold :0
  548. S: FAIL METADATA KEY_NO_PERMISSION * draft/persistence/hold :Key is server-managed and cannot be set directly
  549. C: PERSISTENCE SET OFF
  550. S: :server PERSISTENCE SET OFF
  551. S: :server PERSISTENCE STATUS OFF
  552. ```
  553.  
  554. ## Errata
  555.  
  556. None at time of publication.
  557.  
  558. [batch]: ../extensions/batch.html
  559. [message-tags]: ../extensions/message-tags.html
  560. [metadata2]: ../extensions/metadata-2.html
  561.  
  562. ## Residue from #104 (2026-08-28) — CLOSED same day
  563.  
  564. Both items below were implemented later on 2026-08-28 (PERSISTENCE LIST
  565. subcommand + `list` token restored; truncation signaled by presence/
  566. absence of `draft/chathistory-end` on the inner batch opener, limit+1
  567. probe, spec'd in areas/draft-persistence-spec.md "Auto-replay
  568. completeness"). Remaining: federated replay legs carry no completeness
  569. signal (documented in spec).
  570.  
  571. - **PERSISTENCE LIST unimplemented** — spec defines it (SESSION/ENDOFLIST
  572.   replies, valid post-SASL pre-CAP-END for pre-registration session
  573.   enumeration; MUST also work post-registration) but m_persistence has no
  574.   subcommand. The misleading `list` CAP token was DROPPED (`d38a5a1`);
  575.   re-add it when LIST lands. Spec-conformant meanwhile (extension is
  576.   gated on the advertisement).
  577. - **Auto-replay truncation is silent** — replay caps per target at
  578.   FEAT_BOUNCER_AUTO_REPLAY_LIMIT (default 100, floored at 100) and
  579.   nothing on the wire distinguishes complete from truncated; a client
  580.   that missed >limit in one channel has an undetectable hole. Rubin
  581.   suggests a marker on the inner batch or a count in the closing NOTICE
  582.   so clients can backfill via CHATHISTORY. Protocol/spec decision —
  583.   NOT built, awaiting design call.

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.