Skip to content

M3-11 Channel export

No Date Ruling Reason
D-M3-11-1 2026-10-20 Range rule of exportChannel: from must be strictly before to; an empty or reversed range is 400 validation_failed with one field error, pointer #query/to, code invalid, checked before any lookup. There is no upper bound on the length of the range. openapi states no rule, and a check without a lookup cannot reveal whether the channel exists; no configuration key names an upper bound, http.transfer_timeout bounds one response, and clients split long ranges.
D-M3-11-2 2026-10-20 Line content: every line is the shared Message representation of M1-57 with the full content_md, never cut at message.list_content_max_bytes, so content_truncated is absent; my_reactions is absent; tombstones are included in the representation of M1-57 at their thread_seq. attachments, reactions, pinned and components are whatever the shared representation emits. An export is the complete record of the channel; my_reactions is the caller's state, not channel content, and optional in Message; a tombstone keeps thread_seq and shows no content (04 "Thread sequence"); one representation lets the export follow M3-07, M1-59 and M2 without a change.
D-M3-11-3 2026-10-20 Order: threads ascending by their UUIDv7 id, the messages of a thread ascending by thread_seq. The pair (thread_id, thread_seq) is the keyset of the walk; thread pages (ListExportThreadIDs) and message pages (ListExportThreadMessages) both use the page size api.page_size_max, and a batch is one message page. openapi fixes only "thread_seq order per thread"; UUIDv7 ids are ordered by creation and order never comes from timestamps (03 "Principles" items 1 and 2), so the pair is a total, resumable order in which every message of the selection is emitted exactly once.
D-M3-11-4 2026-10-20 Audit: exactly one allow entry export.channel with resource kind channel, the channel id and the details from and to only, recorded with auditing.Recorder.Record in its own unit of work and committed before the status line is written. If that unit of work fails, the request ends with the problem of the error kind and no line is sent. No export without its audit entry (T-AZ-04); the entry names the range and not the result size, because the size is known only at the end and the chain only appends.
D-M3-11-5 2026-10-20 Batches: there is no snapshot across batches. Before every page the stream starts a fresh request memo (authorize.WithoutRequestScope, then authorize.WithRequestScope; kernel/authorize is unchanged), runs the credential re-check (D-M3-11-9), checks channel.read in filter mode and takes Scope(message.read) at that moment; any refusal ends the stream with ErrExportAborted. Every row passes message.read in filter mode on the same memo; a refused row is dropped, logged and counted in bothy.authz.errors as filter_mismatch. The capability export and the step-up are checked once, at the start. Each line shows the rights at the time of delivery (07 8.2 item 2, 04 8.1), and no transaction stays open while lines go out (02 8.5); one memo per batch keeps a revocation from looking like a filter mismatch. The action is the start of the export (05 2.4), so an ageing step-up does not cut a running export, but a lost read right does.
D-M3-11-6 2026-10-20 Abort: after the first byte, an aborted stream, a read error, the deadline or a shutdown logs only a cause code and the route pattern and panics with http.ErrAbortHandler, so HTTP/1.1 ends without the final chunk and HTTP/2 resets the stream. A write error (client gone) ends the stream quietly. A range without messages gives 200 with an empty body. After the status line no problem document can follow; a cut transfer is the only signal a client cannot take for a complete export, and an empty selection is a valid, complete export.
D-M3-11-7 2026-10-20 Transfer deadline: the route flag Download of M1-17 already marks every operation whose 2xx media type is not JSON, application/x-ndjson included, and StageRoute applies http.transfer_timeout to it. Enabling exportChannel sets the flag, so the route generator and StageRoute need no change and nothing is renamed; exportAuditEvents has had the flag since M1-50. The existing flag already has the meaning the brief asked for (02 14.2 "upload and download"); without it a long export would end after http.request_timeout.
D-M3-11-8 2026-10-20 The export streams NDJSON and stores nothing in files; export packages in files belong only to the privacy export (M7-06). The bullet of 02 section 4.6 is corrected accordingly. openapi exportChannel streams the export (06 "Basic rules": NDJSON lets large exports stream), and 03 files.purpose has no value for a channel export.
D-M3-11-9 2026-10-20 Credential re-check: before every page the stream calls kernel/actor.Authenticator.Revalidate through the messaging port CredentialCheck; internal/wire binds it to the Authenticator of identity lazily, because the session start step runs after the chat step. A refusal ends the stream like a lost read right (ErrExportAborted). A revoked session or device raises no rights version, so the cached liveness facts of authz would keep allowing a stream that outlives its request (T-AZ-03).
D-M3-11-10 2026-10-22 The ticket was built on an integration branch on top of the stacked M3-05 build and is ported to main as its own pull request with only its own commits; generated files are regenerated on main, and the parts of the integration that belonged to other tickets (the branding logo of M3-10 in the route table) are left out. Since 2026-10-21 every ticket lands on main on its own; M3-05 and M1-57 are on main, M3-10 is not.
D-M3-11-11 2026-10-20 test/export expects the run anchors (kind = run) of M2-05 in the threads it creates: the range of TestExportChannel, the batches of BatchBorders and the message list of the thread in Eskalation take them from the database (runAnchors). The export itself is unchanged and streams the anchor like every other message of the channel. Since M2-05 the first message of a new thread in a channel with an agent opens a run whose silent anchor is a message of the thread; the test was written on a base without runs.
D-M3-11-12 2026-10-20 NoTransactionBetweenBatches counts only connections of bothy_app that are idle in a transaction which began and went idle before the pause (xact_start and state_change before clock_timestamp() at the start of the pause), and takes its first sample one sampling interval after the pause begins. The export is unchanged. Since M2-05 and M2-20 the server runs short background units of work during the pause (runs, presence), each idle in a transaction for microseconds between two statements; a unit of work of the export left open between two batches still began before the pause and stays idle through it, so the check keeps its meaning.