Skip to content

M1-61 Read state, drafts and personal settings

No Date Ruling Reason
M1-61/0 2026-10-15 Base int/m1-wave21 at 8b981ed, branch feat/M1-61-sync-personal-data in its own worktree; the branch is not merged locally and not pushed, the integration lane ports it. The migration takes the next free number of the base, 00041. The brief, spec and contract were read from origin/main; the base holds every rule the ticket needs (03 read_states, drafts, user_settings, 04 section 14, 05 personal actions and events, R-AZ-21, 01 section 11, 13 section 3.3, the OpenAPI operations and schemas are the same apart from translation), so no contract commit was brought in. The test of the setting registry finds the chapters by their number (docs/spec/01-*.md, 13-*.md) and their section numbers, so it reads the German chapters of the base and the English ones of main. make check runs with SONAR_SKIP=1 as the launcher sets it. Instructions of the launcher, which override the brief's merge step; chapter names differ between the base and main.
M1-61/1 2026-10-15 A setting that was deleted starts again at version = 1 on its next PUT; the settings.updated of the delete carries the last value with the last version plus one and deleted: true. Brief note 16: clients drop the local entry on deleted: true, so the new entry with version 1 applies; keeping a tombstone row only for the version would store a default the server must not store (04 section 14, row Settings).
M1-61/2 2026-10-15 draft.deleted carries no resource.version, although the brief's event table names version: last + 1. The catalogue (docs/spec/actions.yaml, ADR 0049) records resource_version: false for draft.deleted, and the frame conformance (G9) checks every delivery against the catalogue; the core contract is not edited by this ticket. Clients apply it in seq order (04 section 9 point 3). The empty final state of a PUT still answers version last plus one in its body.
M1-61/3 2026-10-15 The six personal actions of this ticket (read_state.update, read_state.mark_unread, draft.read, draft.write, settings.read, settings.update) are refused to agents and apps by authorize itself: the requirement table of authz has override rows All(Human, Self) and All(Human, Self, Level(read)) with the unchanged 05 texts. A person with a personal token passes. An agent or app without read on the thread or channel still gets 404 (visibility comes first). Brief note 22: permission logic only in authz; the requirement step of M1-28 let an agent naming itself pass self. The table already holds overrides by action id (message.create, thread.tags.set).
M1-61/4 2026-10-15 For clear_draft, OnMessagePosted deletes the author's channel draft of ChannelID when CreatedThread is set and the author's draft of ThreadID otherwise; it does not read the channel kind. Only messages of people clear drafts. Only a forum has channel drafts (putChannelDraft refuses other kinds and a channel never changes its kind), so a new thread of a text channel finds none, exactly as brief note 12 requires; agents and apps have no drafts.
M1-61/5 2026-10-15 ReadState.mention_count counts above last_read_seq with messaging.Mentions.CountUnread, without threads.unread_floor_seq. M1-60/2: imports write no mentions, so the floor hides none; the list, the answers and the events then count the same way.
M1-61/6 2026-10-15 An over-long draft is 413 payload_too_large, not 400 with the field code too_long. U+0000 in a draft is 400 on /content_md with invalid; a channel draft in a channel that is not a forum is 400 validation_failed without a field; field codes are out_of_range, too_many_items, duplicate, not_uuid_v7, not_in_thread, not_allowed (reply on a channel draft) and invalid; a setting value whose event would not stay below sync.event_payload_max_bytes is 400 on /value with too_large. Brief note 8 asks for M1-56's answer to an over-long message (domain.CheckContent); PostgreSQL text cannot hold U+0000 (M1-56/5); a setting has no reduced payload, and the appender would refuse the event with 500.
M1-61/7 2026-10-15 The reply target of a thread draft is checked through the new port messaging.ReplyTargets (InThread, implemented by the messaging service with the rule of createMessage); the chat port gains Threads.InfoForShare and ThreadInfo.LastMessageSeq and UnreadFloorSeq; Structure.ListSidebarStates of M1-52 is used as it is. The kind and space of a channel draft come from Structure.GetChannel after draft.write allowed the call. Brief note 8 asks for the messaging port, and the base port had no reply check; additive changes only.
M1-61/8 2026-10-15 internal/wire builds the subscribers of sync (syncapp.Subscribers) with the module objects of the txn registry and binds the appender in the start step of authz (and in bothy setup) next to the rights hooks; an unbound subscriber fails a message with clear_draft. The golden file of the registrations gains subscribe messaging.MessagePosted -> sync. The test world of messaging takes the option WithPersonal, which registers the subscriber of sync before the test subscriber and adds the twelve handlers; its people authenticate through the test authenticator of httpservertest as for M1-56 and M1-60, not with DPoP proofs. The registry is built before the appender exists (02 section 8.3 "In-Tx events": one list); the world of the base is the shared HTTP world of chat and messaging.
M1-61/9 2026-10-16 Port onto main: code and tests as in the integration squash except the points named here. The migration is 00042_sync_personal_data.sql (after 00041; the integration file was 00042, INT-W25/1), and its header and migrations/sync_personal_data_test.go name the English chapter 03-data-model.md, so its checksum is recorded anew. M3-13 is not on main and threads has no search columns, so ShareThread keeps the ticket's SELECT * and the adapter passes the row as LockThread does (INT-W25/3 is not ported). The messaging test world keeps Option func(*messagingapp.Config) and builds the personal data through NewPersonal (INT-W25/2), without the app installations of M2-24; ruling M1-61/8 keeps the name WithPersonal the ticket wrote. M2-24, M2-26, M2-39 and M2-40 are not on main, so their route rows, wiring and privilege rows stay out (union of INT-W25/1 on the M1-61 side only); the helper input of the authz decision tests, which the ticket took from the M2-39 test file, moves into decide_personal_test.go. The changelog and the READMEs name the agent answer 403 forbidden (reason subject_kind_not_allowed). The multi-byte content of TestDraftEventTruncated and TestTruncateContent is the Greek letter alpha instead of a German umlaut (still one code point of two bytes), because the language gate (D-W-94) rejects umlauts in tracked files, as M1-60/6 did. Generated files are regenerated, not merged. main holds M1-56 to M1-60 but not M2-24, M2-26, M2-39, M2-40 or M3-13; migration numbers follow without a gap.
M1-61/10 2026-10-20 After M3-13 landed on main, ShareThread lists its columns and the adapter converts the row through threadRow, like every other thread read; this replaces the SELECT * point of M1-61/9. D-M3-13-10: no thread read carries search_language or title_search, and a SELECT * row no longer matches threadRow.