Skip to content

M2-04 Trigger decision

No Date Ruling Reason
D-M2-04-1 2026-10-20 The trigger decision is split into Decide and DecideCommand (steps 1 to 6 of 09 3.4, the voice rule of 09 13 and 10.4) and Settle (step 7); the package internal/runs/domain never calls authorize, every permission fact (AuthorHasTrigger, WriteOnChannel) is an input gathered by the caller from authz, and Decision.Check names the check that runs next (CheckInvoke, CheckFollowUp). Place: internal/runs/domain/trigger.go, trigger_command.go, trigger_settle.go, runs README "Trigger decision". Only authz holds permission logic (D-B7, ADR 0017). The operator and guest rules appear twice by design: here they choose the candidate and the reason the person sees, the agent.invoke guard of M1-28 re-checks them under the permission snapshot; when the two disagree, authz wins through Settle. Every outcome that can lead to a run or a delivery carries a check, so no path invokes an agent without agent.invoke (T-AI-08).
D-M2-04-2 2026-10-20 The voice rule (voice_unsupported, 09 13) runs after step trigger_right and before the target is chosen. Transcription path: voice.transcription_mode = service, or agent and the candidate's effective voice_input; off has none. With a path, Decision.Transcript is service or agent; a voice message that would not trigger stays none. Place: trigger.go, 09 3.4. 13 says "rejection at acceptance" but 3.4 does not place it. The path depends on the candidate, so it cannot run before step 3, and a message that cannot be understood must create neither an auto thread nor a follow-up, so it runs before steps 5 and 6.
D-M2-04-3 2026-10-20 Settle maps the deny reasons of authorize to trigger_state_reason: agent_not_reachable with an inactive candidate to agent_disabled, with an active one to agent_not_allowed_here; level_insufficient and no_grant to no_trigger_right; every other reason unchanged. none keeps trigger_state NULL, reject gives failed with its reason whatever the verdict, an allow gives received; command decisions never set a trigger state; a denial with an empty reason is an internal error. Place: trigger_settle.go, 09 3.4 and 3.5. The closed deny list of 07 4.1 has no agent_disabled, and the M1-28 guard answers agent_not_reachable for an inactive agent, while 09 3.5 shows the person "agent disabled". The audit of the denial stays with authz (05 2.4).
D-M2-04-4 2026-10-20 The caller builds the agent.invoke resource of the acceptance check on the thread of M, also when the target is a new thread, with InvokeFacts.AgentID = the candidate, TriggerOrigin = human, MentionsAgent = Decision.Mentioned and ActiveRunAgentID zero for a new thread. The M1-28 guard is not changed. Place: runs README "Where each input comes from". 05 section 5 point 3 names "the effective operator of the channel in the main thread and for new threads", and the mention rule that chose the candidate is the one of the main thread. The re-check before each delivery (04 15.3 point 2) belongs to M2-08.
D-M2-04-5 2026-10-20 Decide and DecideCommand validate first and return an error with code internal_error and the zero Decision (outcome none, no check) for an unknown value of Origin, ChannelKind, ThreadKind, BillingMode or VoiceMode, ThreadOperator on a main thread (R-DM-21), a main thread in a forum channel (R-DM-11), an operator on a DM channel, and a zero ID of the message, thread, channel, command, a mentioned agent, an operator, the command owner or the active run. The error names the field, never a value. Outcome is an integer type whose zero value is OutcomeNone. Place: trigger.go, trigger_reasons.go. Fail closed (09 1 point 4): a broken input never yields invoke, followup or control; the zero value of a Decision built by mistake is also no invocation.
D-M2-04-6 2026-10-21 Port onto main from the ticket branch (own commits 10a504f..401fbe5, after the merge of M2-02, which is on main as #225): the code files are taken unchanged except that doc comments name the English chapter 09-agents.md; the additions to 09 3.4 and 3.5 are placed in 09-agents.md; the runs README on main is English, so its code line is translated and the section "Trigger decision" is added as written; the changelog and this log take only this ticket's lines. TestRejectionTableCoversSpec finds the chapter as docs/spec/09-*.md and the table by its heading number 3.5. Only the ticket is ported; English documents on main win.