| M1-56/0 |
2026-10-14 |
Base int/m1-wave17 at e2f9eb4, branch feat/M1-56-messaging-send in its own worktree; the branch is not merged locally, the integration lane ports it. The migrations take the next free numbers of the base, 00029 and 00032. Text this ticket adds is English. The methods of messaging.Messages take the caller as a actor.Actor like the chat ports, and MessagePosted uses ids.ID where the brief writes uuid.UUID. The start check D0 ran but gave no verdict: the shared Go build cache of the host lost files during the run, and the repeat raced with the first edits of the worktree; the final make check of the ticket covers the base. |
Instructions of the launcher, which override the brief's merge step and language; kernel/ids is the ID type of every port. |
| M1-56/1 |
2026-10-14 |
The foreign key threads.parent_message_id -> messages(id) (DEFERRABLE INITIALLY DEFERRED) is added in 00032 NOT VALID and validated by 00033, as 00025 and 00026 did for grants.target_channel_id. Test code of chat that created threads from invented parent ids now inserts a parent message first, and the schema test of 00025 lists the new key. |
upgrade.md rule M5: a new constraint on an existing table starts NOT VALID; before M1-56 no thread could name a parent, so the validation finds nothing. |
| M1-56/2 |
2026-10-14 |
The mention scanner is code of messaging/domain, no Markdown library: CodeMask follows the block structure of CommonMark 0.31.2 (block quotes, list items, lazy continuation, headings, thematic breaks, fenced and indented code) and the inline rules of backslash escapes, autolinks and code spans, and its tests hold every code-span and code-block example of the specification (107 to 147, 328 to 349). Raw HTML is not part of content format 1, so an HTML tag is text and its backticks can open a code span; example 344 is the one case that differs from CommonMark. |
Brief D5 allows a scanner that passes the examples; no new dependency, license or vendor group. SEC-OUT-1 removes raw HTML from the dialect. |
| M1-56/3 |
2026-10-14 |
The port internal/messaging depends on no other module (02 section 7.2, make arch): ThreadCreated names the thread by id, and the handler of createThread reads the thread through chat.Threads.Get after the unit of work, so the answer shows last_message_seq, message_count and version after the first message. messaging.Message has no attachments, reactions, components or pins yet; the REST form and the events send them empty and pinned: false. |
go-arch-lint allows a module port only kernel ports; the adapters may use the chat port. |
| M1-56/4 |
2026-10-14 |
Mentions (07 section 4.6): a person is mentioned when the author may read the person (user.read) or the person may read the thread; the second check runs for every author, as 07 states it, and is decisive only for authors without user.read. The person is checked as a principal without a session, and authz gives the owner role only in a session, so an active owner (from the directory) counts as a reader of every thread. The checks run in filter mode; no audit entry for a token that stays text. |
07 section 2.1: owners read everything; without the owner rule an agent could not mention an owner who holds no grant. |
| M1-56/5 |
2026-10-14 |
Body rules beyond the brief: content_md with U+0000 is 400 on /content_md with code invalid; an empty attachment_file_ids and an empty components are accepted, only non-empty ones are not_enabled; an explicit kind: "text" and an absent kind give the same request hash. |
PostgreSQL text and jsonb cannot hold U+0000, which would otherwise be a 500; brief D2 refuses only non-empty lists. |
| M1-56/6 |
2026-10-14 |
A lost race on (author, client_msg_id) answers from a second unit of work like a repetition. When the request already runs inside an outer unit (only createThread with both Idempotency-Key and first_message), the transaction is lost with the unique violation, so the answer is 409 conflict and the client repeats. |
04 section 5.1 point 4; a nested unit cannot start a new transaction. |
| M1-56/7 |
2026-10-14 |
message.created is encoded without HTML escaping, so the payload size follows the content. A payload of at least sync.event_payload_max_bytes gets the longest valid UTF-8 prefix of content_md that fits (binary search over character boundaries), content_truncated: true and the envelope flag truncated; the system line of a reopen is a message.created without the client_msg_id echo. |
04 payload rule 2 (the appender refuses, the producer cuts); brief D7. |
| M1-56/8 |
2026-10-14 |
The HTTP world of the tests is services/server/test/messagingworld (like test/terminalworld), so the tests of app, adapters/httpapi and adapters/postgres each measure their own package. The test subscriber of MessagePosted is a method of a type in each test package, because the txn registry derives the module of a handler from its package. |
Coverage is per package (make coverage, SonarQube); the registry refuses handlers outside internal/<module>. |
| M1-56/9 |
2026-10-14 |
Test first held for the identity lookup and the owner mention rule (red runs in the report); the domain tests, the migration test and the HTTP tests over PostgreSQL were written before or together with their code and run red on test defects first, not on missing code. |
Honest record of the order of work. |
| M1-56/10 |
2026-10-14 |
Code review: the scanner kept the container stack in a form where a blank line walked every open list item, a line of bullets rechecked the thematic break over the rest of the line per marker and a block quote searched the innermost item backwards, so a message of message.max_content_bytes with thousands of nested containers took seconds of CPU. The parser now keeps the first container a blank line stops at, the last non-break byte of each line and the indexes of the open items, so every step is constant time; a test bounds six such inputs. The message after the thread lock takes channel and space from the locked thread. |
A send request must not occupy a core; no nesting limit is needed, so CommonMark stays unchanged. |
| M1-56/11 |
2026-10-14 |
Red team: the createThread answer reads the thread through the new chat.Threads.View, which does not authorize, instead of Get (thread.read); a replay of the first message authorizes message.create on its thread before it answers. The scanner treats a line that is blank after its block quote markers like a blank line for the containers inside the quote (CommonMark 0.31.2 example 262), keeps the stops of a blank line in a sorted list and scans the indent of a container only as far as it needs. |
05 section 2.6 asks only messages:write for thread.create; a 403 after the commit hid a stored post. The code mask must match CommonMark and stay linear (M1-56/10). |
| M1-56/12 |
2026-10-15 |
Port onto main: code and tests are taken byte for byte from the integration squash, apart from the points named here; of the integration fixes of wave 19 only the unions of INT-W19/1 and INT-W19/6 concern this ticket (section "Integration int/m1-wave19"). The integration base also holds M2-24 (apps), M2-39 and M2-40 (terminal), which are not on main, so nothing of them is ported: terminal.Register stays out of moduleRegistrations, the terminal and apps fields, their start steps and the Hosts handler stay out of wire/serve.go, the host and app route rows stay out of api/routes/real_test.go, the host and terminal tables stay out of migrations/privileges_test.go, and the terminal and apps entries stay out of sqlc.yaml. The migrations keep the ticket's numbers 00032_messaging_messages.sql and 00033_threads_parent_fk_validate.sql, which follow 00029_identity_local_credentials.sql without a gap (the integration renumbered them to 00034 and 00035, INT-W19/3); the changelog, the module README and the comment of migrations/chat_channels_test.go name them, and TestMessagingMigration_Down migrates down to version 29 instead of 31. The header of 00032_messaging_messages.sql names the English chapter file, 03 principle 7 and 00025 as the migration that created threads on main, so its checksum differs from the integration file and is recorded anew; M1-56/1 names the main numbers 00025 and 00026 of the channel migrations. The comments of queries/messages.sql, queries/mentions.sql, the messaging repository and messaging.go and the schema test name the English chapter files and terms (03-data-model.md, 05-actions.md, 03 principle 12), and the new key of wire/config_test.go names the chapter 03-data-model.md. The changelog entry, the chat and messaging READMEs (status lines, code list, the new section on sending, the heading "Content format" of 03 messages), the key row of docs/operations/configuration.md and these rulings are written into the English text at the same places. Generated files are regenerated with the generators of the ported tree, not merged. |
main is English and holds M1-55 and M1-39 but not M2-24, M2-39 or M2-40; the migration numbers of main must follow without a gap. |
| M1-56/13 |
2026-10-20 |
Pull request review: the CHECK of messages.system_code holds both codes of 03, thread.reopened and agent_session.resume_failed (brief D6: the CHECK holds every code of 03); M1-56 writes only thread.reopened, M2-12 the other. |
Brief D6 and 03 messages; the first port held one code. |
| M1-56/14 |
2026-10-20 |
SonarQube found 3.8 % duplicated new code (gate 3 %): every module copied the same small helpers (API time, optional id, event actor, uuid and timestamptz columns, optional response members). They are shared now: clock.FormatAPI, ids.ID.StringOrEmpty, eventlog.ActorFor, pg.UUID, pg.NullableUUID, pg.IDOf, pg.TimeOf, rest.OptionalID, rest.OptionalTime and rest.IDs, each with a test. messaging keeps its local names as aliases of them; the copies in the older modules stay until a ticket touches them. eventlog.ActorFor takes the kind as text, because kernel ports may not import each other (.go-arch-lint.yml). |
The duplication gate fails each new module that copies them; one shared place fixes the class. |
| M1-56/15 |
2026-10-20 |
Port onto main after M2-24, which took 00030 and 00031: the migrations of the M1 stack move two numbers up (00032_messaging_messages.sql and 00033_threads_parent_fk_validate.sql here), with every reference, the down test (DownTo 31) and the checksums; the sqlc models are generated again. The test world test/messagingworld gives an app an active installation with its scopes, because authz now reads the installation of an app (M2-24). |
Next free numbers on main; an app without an active installation has no rights since M2-24. |