Skip to content

Implement terminal flag (SPEC v0.6.0) - #43

Merged
markmnl merged 3 commits into
mainfrom
terminal-flag
Sep 2, 2026
Merged

Implement terminal flag (SPEC v0.6.0)#43
markmnl merged 3 commits into
mainfrom
terminal-flag

Conversation

@markmnl

@markmnl markmnl commented Sep 2, 2026

Copy link
Copy Markdown
Owner

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.

  • Flag: FlagTerminal = bit 6. Only bit 7 is reserved now; reserved-bit rejection is unchanged otherwise.
  • Header validation: an add-to message carrying terminal is rejected with code 1 without any lookup, since an add-to copies the original's flags (SPEC §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.
  • Schema: new msg.is_terminal column (idempotent add column if not exists), persisted from wire flags and loaded back into every outgoing wire form.
  • Triggers: setting pid to a terminal parent, or inserting an msg_add_to_batch for a terminal message, raises. This makes the Sending Host rule hold for rows created by fmsg-webapi too.
  • Sender: defensively records code 1 (not retryable) rather than transmitting a reply to a terminal parent or a batch of a terminal message (§10.2).
  • Testability: the store lookups used by the reply and add-to validation paths are now overridable function vars, following the existing threadHasFromDomainFn pattern.
  • SPEC.md synced from fmsg-spec v0.6.0.

Common Media Type IDs on the wire (added for FMSG-005, which requires ID 56 for reactions)

  • The sender previously encoded every type as a string. Outgoing headers now set the common type flag and one-byte ID for the message type and each attachment type that has one (SPEC §4/§5).
  • Because the message hash covers the header exactly as transmitted, a message whose hash was recorded by an earlier delivery keeps the form that reproduces it: the common-type form is tried, its hash compared with the stored one, and string types are used on mismatch. Add-to batches follow the original's form; batch hashes are now loaded so a batch with its own stored hash is checked the same way.
  • Fixes originalHeader sharing the loaded attachment slice between wire forms, which let flags set on one header leak into the next.
  • Merged main (Remove unreachable INSERT branch from storeMsgHeaderOnly #37) and resolved the storeMsgHeaderOnly conflict by keeping main's removal of its INSERT branch.

Verification

  • go build ./..., go vet ./..., go test ./... pass.
  • dd.sql applied 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)

  • fmsg-docker: sync its copy of fmsgd/dd.sql once this merges.
  • fmsg-webapi: set is_terminal when 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

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
markmnl and others added 2 commits September 2, 2026 15:34
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>
@markmnl
markmnl merged commit b55b12f into main Sep 2, 2026
2 of 3 checks passed
@markmnl
markmnl deleted the terminal-flag branch September 2, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant