Implement terminal flag (SPEC v0.6.0) - #43
Merged
Merged
Conversation
A terminal message is a leaf of its thread: no message may reference it via pid. Per SPEC v0.6.0: - pkg/fmsg: FlagTerminal (bit 6); only bit 7 remains reserved. - Header validation: an add-to message with terminal set is rejected with code 1 without a lookup, since an add-to copies the original's flags and so signals the original is terminal (§10.3 step 3). - Reply path: a reply whose stored parent is terminal is rejected with code 1 (§10.3 step 7). Add-to path: a batch whose stored parent is terminal is rejected with code 1, catching a sender that stripped the bit. - Store: new msg.is_terminal column persisted from the wire flags and loaded back into every outgoing wire form via baseFlags. - dd.sql: replying to a terminal message (pid trigger) and adding a batch to one (new msg_add_to_batch trigger) raise, so neither fmsgd nor fmsg-webapi can create a unit the Sending Host must not send. - Sender: defensively records code 1 (not retryable) instead of transmitting a reply to a terminal parent or a batch of a terminal message (§10.2), so such rows stop being retried. - Store lookups used by the validation paths are now overridable function vars so the reply and add-to paths are unit-testable. - SPEC.md synced from fmsg-spec v0.6.0. Verified: go test ./... passes; dd.sql applied to a scratch PostgreSQL, both triggers refuse the terminal cases and allow ordinary ones, and the script re-applies idempotently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MW4FXDGacoWXeKPU5cQTHS
This was referenced Sep 2, 2026
storeMsgHeaderOnly's INSERT branch was removed on main (#37); the is_terminal column is inserted by storeMsgDetail only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MW4FXDGacoWXeKPU5cQTHS
The sender always encoded types as strings, so a host could never send the one-byte common type form (SPEC §4) that FMSG-005 requires for reactions (ID 56). Outgoing headers now set the common type flag and ID for the message type and each attachment type that has one. The message hash covers the header exactly as transmitted, so a message whose hash was recorded by an earlier delivery keeps the form that reproduces it: encodeForWire tries the common-type form, compares its hash with the stored one, and falls back to string types when they differ. Add-to batches follow the original's form, and a batch with its own stored hash is checked the same way (batch hashes are now loaded). Also fixes originalHeader sharing the loaded attachment slice between wire forms, so flags set on one header leaked into the next. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MW4FXDGacoWXeKPU5cQTHS
markmnl
added a commit
to markmnl/fmsg-webapi
that referenced
this pull request
Sep 2, 2026
Requires fmsgd's msg.is_terminal column (markmnl/fmsgd#43). - Message objects gain `terminal` (settable on create/update), `reaction` (the emoji a message carries when it is a reaction, "" for a clearing one, null otherwise) and `reactions` (effective reactions on the message: each participant's latest by time, ties by message hash, grouped by emoji). - A reaction is recognised by its FMSG-005 shape: a reply with no_reply and terminal set, not important, no add-to, type text/plain;charset=UTF-8, no attachments, body a single emoji or empty. - POST /fmsg/:id/react sets or clears the caller's reaction by sending a reaction message to every other participant in one step; idempotent when the reaction is unchanged; same reply-domain check as send. - Replies to a terminal message and add-to on one are refused with 409 before the database trigger would. - WebSocket: an arriving reaction is pushed as a `reaction` event carrying the refreshed subject, never as new_msg; no Web Push. - New internal/emoji package: RGI_Emoji set and emoji property tables generated from Unicode 17.0 data (go run ./internal/emoji/gen), with IsRGI, IsPossible (UTS #51 possible_emoji, emoji presentation required) and IsSingle. - README documents the route, fields and event. Verified: go test ./... passes; the new SQL was executed against fmsgd's v0.6.0 schema in a scratch PostgreSQL, including the trigger refusing a reply to a reaction. Claude-Session: https://claude.ai/code/session_01MW4FXDGacoWXeKPU5cQTHS Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the terminal flag from fmsg Specification v0.6.0 (markmnl/fmsg#32). A terminal message is a leaf of its thread: no later message may reference it via pid. This is the host-side mechanism behind FMSG-005 Reactions.
FlagTerminal= bit 6. Only bit 7 is reserved now; reserved-bit rejection is unchanged otherwise.msg.is_terminalcolumn (idempotentadd column if not exists), persisted from wire flags and loaded back into every outgoing wire form.pidto a terminal parent, or inserting anmsg_add_to_batchfor a terminal message, raises. This makes the Sending Host rule hold for rows created by fmsg-webapi too.threadHasFromDomainFnpattern.SPEC.mdsynced from fmsg-spec v0.6.0.Common Media Type IDs on the wire (added for FMSG-005, which requires ID 56 for reactions)
originalHeadersharing the loaded attachment slice between wire forms, which let flags set on one header leak into the next.main(Remove unreachable INSERT branch from storeMsgHeaderOnly #37) and resolved thestoreMsgHeaderOnlyconflict by keeping main's removal of its INSERT branch.Verification
go build ./...,go vet ./...,go test ./...pass.dd.sqlapplied to a scratch PostgreSQL 16: a reply to a terminal message and an add-to batch on one both raise; ordinary replies and batches still succeed; the script re-applies idempotently.Follow-ups (other repos)
fmsgd/dd.sqlonce this merges.is_terminalwhen creating reactions (FMSG-005) and expose the flag; refuse replies to terminal messages at the API layer with a clear error rather than surfacing the trigger exception.🤖 Generated with Claude Code
https://claude.ai/code/session_01MW4FXDGacoWXeKPU5cQTHS