| D-M2-15-0 |
2026-10-22 |
Built on the old integration line (with M2-08 and M2-09 merged in) and ported to main after M2-09 landed: only the ticket's own commits were brought over and adapted to main (M2-16 slash commands and the module wiring of PL/28 landed meanwhile). The additions to the German chapter files of the old base go to their English files on main; the migrations take the next free numbers on main, 00061 to 00063; the wiring of the component source moves into wire/module_messaging.go and wire/module_runs.go. |
The rules of the landing replace the integration branches; main is the reference. |
| D-M2-15-1 |
2026-10-21 |
A change of the state or the definition of a component raises messages.version of its card by one and appends message.updated with the card and its current components, through the port method messaging.Messages.ComponentsChanged; the caller holds the card lock of Messages.LockCard. Place: messaging/app/cards.go, 02 section 4.7. |
One event type has one producer (02 4.7, as SetTriggerState for trigger_state), and 05 section 6 names the component state as content of message.updated, whose catalogue entry carries resource.version, so the version must rise (03 messages.version). |
| D-M2-15-2 |
2026-10-21 |
messaging declares the source port ComponentWriter (Check without database access, Attach in the ambient unit of work) next to ComponentReader (ForMessages); runs implements both in adapters/messagingsource. Attach returns the stored components in position order, and CheckedCard carries the components as they will be stored, so messaging builds message.created and the size rule without a second read; CardInput carries the request time. Place: messaging/components.go, runs/adapters/messagingsource, 02 section 4.3. |
Component definitions are content that MessagePosted never carries (M1-56), a card must be refused before its message exists, and messaging may not import runs (02 4.1 point 4, 4.4). |
| D-M2-15-3 |
2026-10-21 |
The open points of component.v1: an empty card and a card above component.max_per_message give only that one finding on /components, every other check reports every finding; an empty required text is required; an empty optional text (placeholder, option description, submit_label) is left out of the stored form; a member outside the closed set, JSON null as a value and a repeated key are invalid, and the pointer of a member outside the set names the object, never the member; an empty entry of choices is invalid; the stored definition is the typed form re-encoded in a fixed member order with the defaults applied (button style, select min_values and max_values, field required); numbers are compared by value (replay and the no-change test of updateComponent), because the stored jsonb gives 1e2 back as 100. No length limit per member besides the keys of 09 section 21; the card is bounded by sync.event_payload_max_bytes. Place: runs/domain/components.go, runs/domain/values.go, 09 section 11.1. |
The chapter leaves these points open; the request has to change as a whole for the two card-level findings and the work stays bounded by the limit; keys are agent text (SEC-T10); a repeated key is ambiguous because decoders differ in the member they keep; no number without a key. |
| D-M2-15-4 |
2026-10-21 |
The generic errors[].code invalid covers every rule of component.v1 that is not required, too_long or limit_reached (unknown member, wrong JSON type, value outside its list, duplicate, bad bounds, rejected character, expires_at not after the request time). Place: runs/domain/components.go (FieldInvalid), 09 section 11.1. |
06 section 5 point 3 names codes for missing, too long and limits only; one code for the rest keeps the list closed, and detail names the rule. |
| D-M2-15-5 |
2026-10-21 |
Card rules of createMessage after the decoder: components require kind = card (invalid on /kind), kind = card requires a component (required on /components), only agents send cards (people invalid, apps not_enabled until M2-25), run_id only with kind = card (not_enabled otherwise until M2-19; replaced by D-M2-15-29); then ComponentWriter.Check and the size rule (too_long on /components). Attach refuses a card of an agent whose operator_config sets supports_components = false with 403 feature_disabled. Place: messaging/app/cards.go, runs/app/components.go AttachCard, 09 sections 2.2 and 11.4. |
The server enforces the owner's part of the capability (09 2.2, R-AG-01); the adapter's report is M2-20. |
| D-M2-15-6 |
2026-10-21 |
The run of a card lives in message_components.run_id; messages.run_id stays NULL for kind = card. run_id must be an active run of the author in the message's thread, checked under FOR SHARE after the message row is inserted, with one 400 validation_failed with code invalid on /run_id for every failing case (the answer codes are replaced by D-M2-15-29). Place: runs/app/components.go AttachCard, 09 section 11.4. |
03 allows messages.run_id only for kind = 'run' (the check (kind = 'run') = (run_id IS NOT NULL)); one code leaks nothing about other agents' runs; the share lock orders the card against a concurrent end of the run (spec gap reported). |
| D-M2-15-7 |
2026-10-21 |
Request hash bothy-msg-v2 for a createMessage request with components or run_id: the fields of v1 in v1 order, then run_id (empty when absent), then the canonical card (per component in request order custom_id, type, the definition re-encoded from its typed form as received without defaults, single_use, required_capability, restricted_to, expires_at in RFC 3339 UTC with milliseconds), each as uvarint(len) and bytes; a request with neither keeps bothy-msg-v1 byte for byte. Place: messaging/domain/message.go RequestHash, runs/domain/components.go CheckCard. |
M1-56 D3: a new encoding is a new version prefix, never an edit; a message stored before M2-15 still replays; key order and white space of the request do not change the hash (spec gap reported: 04 5.1 point 3 lists neither). |
| D-M2-15-8 |
2026-10-21 |
In the agent.invoke guard of interaction.create the mention rule of point 3 counts as met; a guest agent still needs allow_mentioned_agents, its own write and billing mode organization. Place: authz/domain/guards.go reachable, 09 section 11.2. |
The card is the agent's own address to the person; a click is the answer to it, not an unaddressed message. |
| D-M2-15-9 |
2026-10-21 |
interaction.create on a component without run while the card's agent has an active run in the thread is denied with run_active (at stage accept, after run_active_other_agent). Place: authz/domain/guards.go invokeGuard, 07 section 4.1. |
As a model command during a run (05 command.invoke): a second run cannot exist (R-DM-14), and a follow-up is a message (05 section 5 point 8). |
| D-M2-15-10 |
2026-10-21 |
A component bound to a run that is no longer queued, running or waiting_input is inactive (component_inactive). Stage deliver (the re-check before interaction.created and before agent.invocation of a run an interaction opened) evaluates required_capability and restricted_to with the rights of that moment and skips the state check and run_active. Place: authz/domain/interaction.go componentRules, runs/app/delivery.go deliverFacts. |
The question of an ended run has no one to answer it; at delivery the component is used by this very interaction and the run is the one the event goes to. |
| D-M2-15-11 |
2026-10-21 |
The restriction of a component (restricted_to = run_starter without being the starter or an owner, restricted_to = owner without being an owner) is forbidden component_restricted (403 forbidden with reason); component_inactive keeps its reason in the audit and is answered 409 component_used; the invoke reasons (origin_not_human, agent_not_reachable, billing_mode_personal_not_holder, tool_class_owner_only, project_not_ready, run_active, run_active_other_agent) answer 403 agent_invocation_denied with reason. Place: kernel/authorize/decision.go Decision.Err, 07 section 4.1. |
"Always allow" for non-owners is the same kind of refusal as the run-starter restriction (R-AG-12, D-AG-10); a lost race answers the same whether authorize or the single-use lock decided it (03, 06 5). |
| D-M2-15-12 |
2026-10-21 |
Value shapes: button {}; select {"selected": [...]} (known, unique entries, count between min_values and max_values, absent selected is the empty list when min_values = 0); form an object keyed by field name with typed values; every finding interaction_values_invalid with a pointer that never names a key the person typed (/values for an unknown key); values stored as sent; their interaction.created must fit sync.event_payload_max_bytes (too_long on /values). A repetition of client_interaction_id is compared by the stored component_id and values as JSON (key order ignored, numbers by value) after a read check of the card. Place: runs/domain/values.go, runs/app/interactions.go interactionReplay, 09 sections 11.1 and 11.4. |
09 11.1 names no shapes; keys and values are human text (SEC-T10); 03 interactions stores no request hash. |
| D-M2-15-13 |
2026-10-21 |
A component without single_use stays active and unchanged when used (no version, no message.updated); the interaction row records who and when. Place: runs/app/interactions.go applySingleUse, 09 section 11.3. |
Nothing about the component changes, so there is nothing to announce (06 2 "No effect"). |
| D-M2-15-14 |
2026-10-21 |
The run of an interaction: a component with a run goes to that run; a component without run in a thread without an active run opens a new run with trigger_kind = interaction, triggering_interaction_id, triggered_by the person, the card's agent and thread. Events in this order: message.updated of the card, the events of Open, interaction.created, and for a new run agent.invocation last. Place: runs/app/interactions.go, runs/domain/interaction.go RouteInteraction, 09 section 11.4. |
agent.invocation announces the run and carries the start-bearing token, while the values reach the agent only through interaction.created (run_context.v1 has no place for them, spec gap reported), so they are in the adapter's inbox before the start-bearing event (ADR 0023). |
| D-M2-15-15 |
2026-10-21 |
The delivery sets interactions.state from received to dispatched when it hands interaction.created to a transport and to failed on a denial at delivery (the run unchanged, audit and counter as M2-08); an interaction the agent already answered keeps its state. agent.invocation of a run whose interaction was deleted is checked without Interaction facts (points 1 to 5, 7 and 8 still re-check the person, I2 and SR2); interaction.created of a deleted interaction is not sent, without a check or an audit entry. Place: runs/app/delivery.go (resolveInteraction, resolveInvocationInteraction, setInteraction). |
The hand-over is the delivery the server can observe; a deleted card leaves nothing to deliver, but a run it opened still needs the person's rights. |
| D-M2-15-16 |
2026-10-21 |
respondToInteraction: only the owner of the card, state one of acknowledged, failed, rejected; from received or dispatched only, the same final state again is 200 without a write, every other transition 409 invalid_state_transition; no event; response.text bounded by message.max_content_bytes UTF-8 bytes (too_long on /response/text) and without the text check. The operation gains its 409 response in openapi.yaml. Place: runs/app/interactions.go Respond. |
A hint is message-like text and content (SEC-IN-4 covers display texts); 06 2 "No effect" needs the 409 (spec gap reported). |
| D-M2-15-17 |
2026-10-21 |
Transitions of updateComponent: used and expired are final (an active component past expires_at counts as expired); active -> disabled; disabled -> active only before expires_at and, for a single_use component, only while no single_use component of the message is used; every other change 409 invalid_state_transition; the definition may change in every state; no change answers 200 without a write. A state change of a single_use component locks the whole single-use set of the card. Place: runs/domain/interaction.go StateTransition, runs/app/components.go UpdateComponent. |
The single-use rule must not allow a second decision (09 11.3); the header fields cannot change (SEC-OUT-4). |
| D-M2-15-18 |
2026-10-21 |
Job runs.component_expiry (module runs, queue maintenance, args the card message only) is scheduled with InsertTx and ScheduledAt per card at its earliest expires_at by Attach and by a reactivation; the handler locks the card, sets the due active components expired with version + 1, appends one message.updated and schedules itself for the next expires_at; it is not periodic. Place: runs/adapters/jobs/component_expiry.go, runs/app/component_lifecycle.go ExpireComponents, 02 section 12.3. |
A periodic job with the interval component.default_ttl would leave components active for up to a whole period; correctness never depends on the job, because authorize treats a passed expires_at as expired (02 12.4 rule 1). |
| D-M2-15-19 |
2026-10-21 |
Lock order of domain rows across tables: threads (only where a run may be opened or a message is written), runs (ascending id), messages (ascending id), message_components (ascending id), interactions; the counters last. The one later runs lock is the share lock of Attach after the new card row, which no other transaction can see yet. Remaining deadlocks are retried by the unit of work (database.tx_retry_max). Place: runs/app/interactions.go, runs/app/components.go, runs/app/component_lifecycle.go, runs/README.md. |
03 section 6.3; M2-05 (Transition: runs, then messages) and M1-58 (thread, message, then MessageDeleted) already follow it, and two concurrent clicks serialize on the single-use group (AG-15). |
| D-M2-15-20 |
2026-10-21 |
Audit detail set of an allowed interaction.create on an approve_actions component: interaction_id, message_id, run_id (when set), component_type, required_capability, restricted_to, and choice only for a button whose value is once, session, always or deny; never custom_id, a label, another value or a form value. Other allowed interactions, component.update and interaction.respond write no allow entry; denials are written by authz. Place: runs/domain/interaction.go InteractionAuditDetails, runs/app/interactions.go auditApproval. |
05 4.10 (audit on allow for approve_actions, otherwise denials); SEC-AUD-5: agent and human text stays out of the audit. |
| D-M2-15-21 |
2026-10-21 |
The open components of a run are disabled at every end of the run, not only after a stop, through DisableRunComponents in the end-of-run function endRun; the denial of a queued run at delivery and runs.queued_expiry now go through endRun as well; per card one message.updated. At a follow-up answer to a waiting run the follow-up acceptance of M2-11 calls DisableRunComponents (Followups.store, wired when the port met M2-11 on main). Place: runs/app/finish.go endRun, runs/app/delivery.go (deny, expire), runs/app/component_lifecycle.go, 09 section 11.6. |
A card may already be bound to a queued run, and a question of an ended run has no one to answer it (09 8 point 5); "whatever arrives first counts" (09 11.6). |
| D-M2-15-22 |
2026-10-21 |
Renewal of interaction tokens: renewInvocationToken re-checks a token of trigger_kind = interaction like the delivery it came with (agent.invocation for the interaction that opened the run and for any interaction once runs.triggering_interaction_id is NULL, interaction.created for any other interaction of the run); an interaction.created token whose interaction was deleted is not renewed (403 invocation_token_invalid with reason invalid), while the token of a run whose opening interaction was deleted is checked for the person without Interaction facts (as D-M2-15-15) and renewed on allow. Replaces D-M2-09-23 for the trigger kind interaction. Place: runs/app/renew.go (renewalInput, renewChecked). |
Step 4 of 09 4.5.1 repeats the check of the delivery; a trigger that no longer exists cannot be checked again. |
| D-M2-15-23 |
2026-10-21 |
triggered_by of agent.invocation for a run an interaction opened comes from the new messaging port method Messages.PrincipalOf, the reference of a principal the way TriggerMessageOf returns a message author. Place: messaging/messaging.go, runs/app/interactions.go emitInteractionInvocation. |
runs may not use identity (02 4.4), and kernel/actor carries no handle or display name. |
| D-M2-15-24 |
2026-10-21 |
The owner's switch reaches runs through the agents port: agents.RunFacts.ComponentsAllowed is false exactly when operator_config sets supports_components to false. Place: agents/agents.go, agents/app/facts.go. |
runs reads agent facts only through agents.Facts (02 4.4); a missing key allows (09 2.2). |
| D-M2-15-25 |
2026-10-21 |
Late binding of the component source: internal/wire builds messagingsource.Source before the messaging module, which receives it, and binds it to the runs use cases in NewRuns; an unbound source fails closed (internal error), so a process never shows a card without its components or stores one without them. Place: runs/adapters/messagingsource, wire/runs.go, wire/module_messaging.go, wire/module_runs.go. |
The messaging module is built before runs, which needs its port; the same late binding as the acceptance of runs. |
| D-M2-15-26 |
2026-10-21 |
The size rule of a card reserves every later message.updated: createMessage accepts a card only when its payload fits sync.event_payload_max_bytes with an empty content and with reply_to_message_id, edited_at, pinned, reaction.max_distinct_per_message reactions of reaction.emoji_max_bytes each, and on every component run_id when bound, state disabled, expires_at, used_by and used_at, all IDs and numbers in their longest form. updateComponent checks the same reserve after writing a new definition through the messaging port method Messages.CardFits (400 validation_failed, too_long on /definition). Place: messaging/app/cards.go, runs/app/components.go. |
Components cannot be cut (04 2.2 rule 2), so a card accepted near the limit made every click, reaction-loaded run end and the orphan sweep answer 500 (red team M2-15); checking only message.created let the state changes grow past the limit. A card that keeps the reserve fits every state it can reach. |
| D-M2-15-27 |
2026-10-21 |
Values of createInteraction that hold the escape of NUL or of an unpaired surrogate are interaction_values_invalid on /values; response.text of respondToInteraction with U+0000 is invalid_characters on /response/text. The scan textcheck.StorableJSON moved from the module apps into the kernel and serves both. |
PostgreSQL refuses both escapes in jsonb (SQLSTATE 22P05), which made both requests answer 500 (red team M2-15); the class was fixed for manifests in D-M2-24-21, and one kernel helper now covers every JSON document stored as sent. |
| D-M2-15-28 |
2026-10-21 |
The size reserve of a card (D-M2-15-26) comes from configured bounds only, never from a value the card or its author has when the check runs: message.max_mentions mentions, the author with principal.handle_max_chars handle characters, principal.display_name_max_chars display name characters in their longest JSON form (six bytes) and an avatar, and the member content_truncated of the cut payload; only the components and the bound run, which no later change of the message can grow, come from the card. Messaging binds the two principal keys. A seeded model test (TestCardReserveCoversEveryLaterState) draws later states of the largest accepted card and requires every message.updated payload to fit. Ruling of the class fix round, steering under the owner's rule of 2026-10-17. Place: messaging/app/cards.go cardFits, messaging/app/service.go. |
The delta red team of M2-15 reproduced the class twice more: an edit of the card text to more mentions made every click (and from more mentions the edit) answer 500, and the reserve also counted the author's names as they were at creation and left out content_truncated. A reserve of current values breaks with the next change of any of them; only the bounds hold for every reachable state. The largest accepted card shrinks by the reserved bytes. |
| D-M2-15-29 |
2026-10-23 |
run_id of createMessage after M2-19 landed: it follows the answer rules of M2-19 for every kind (agents only; an unknown run, a run of another thread or of another agent 400 validation_failed with run_mismatch on /run_id; an ended own run 409 conflict with run_not_active); on a card it also binds the components (message_components.run_id) and stays part of the card's request hash bothy-msg-v2, while a text answer keeps bothy-msg-v1 without it. AttachCard checks the run under its share lock with domain.AnswerVerdict and the same mapping as the answer check (answerError), and MessagePosted.RunID is set for a card as for a text answer. Place: messaging/app/cards.go cardRules, messaging/app/create.go, runs/app/components.go checkCardRun, runs/app/answer.go. |
One field, one rule: a card and a text answer that name the same run must get the same answer; the components of a card store the run, so it belongs to the card's canonical request. |
| D-M2-15-30 |
2026-10-23 |
The acting-for binding of M2-14 (SR9) accepts the token of an interaction or a follow-up whose sub is not runs.triggered_by: domain.ActingForVerdict compares sub with the run's triggering person only for the trigger kinds message and command (and autocomplete, which has no run), while UserID of the binding always comes from runs.triggered_by. Replaces "triggered by sub" of D-M2-14-3 for these two kinds. Place: runs/domain/actingfor.go subIsRunStarter, 07 SR9, test TestSR9_InteractionTokenOfAnotherPerson. |
An interaction token names the person who clicked (09 4.5: sub is the triggering person), which may be an owner on a card of Alex's run; the strict comparison made that token, the current one of a waiting run, invalid for every request of the agent, resumeRun included (delta red team of the port). The binding still intersects with the run's triggering person, so the token adds no right. |