ADR-0051: Notices: messages that start no turn¶
Status: Accepted Date: 2026-09-30 Deciders: Alex Nodeland
Context¶
A message committed directly, without Runner.send, starts no turn, but a running turn's watcher still passes it in, and nothing tells clients it differs from a person's message (#63). stackr RFC-0002 has reflexr rules post notices by default (its decision D7): information for people that must not wake or steer the thread's agent. relayr, the bridge, will post them from reflexr runs, as an ExternalAgentActor per rule, and runs retry. A notice needs:
- to start no turn, to steer none and to answer no paused run
- a mark that clients can show
- to stay out of the model's history, unless the application asks for it
- an id that a retry repeats, refused the second time as a message's is (ADR-0045)
Decision¶
- A notice is a message of kind
notice.PostMessageandMessagePostedgainkind,"message"(the default) or"notice". That is additive to protocol v1: an optional field, with a default. - A notice is a message everywhere else. It uses the message id space, so a used
message_idis refused as any message's is (ADR-0045), and feedback targets it withMessageTarget(ADR-0037). - The runner commits a notice as it is.
Runner.executehands only amessagetosend, which starts, steers or answers a run (ADR-0020). A notice is committed like any other command, so it starts no run, and it answers nothing in a paused thread.Runner.sendposts messages only. Every surface can post a notice: the WebSocket and REST in the command, and MCP'spost_messagetool with itskindargument. - The agent isn't told, unless the application asks. The capability's watcher passes a running turn the thread's messages, not its notices. With
ArtifactWorkspace(notices=True), the agent is told about the notices others post in its thread as change notes (NoticeNote: "timeline-entry posted a notice: ..."), when a run starts and while it runs (ADR-0008). A note informs the agent, where a message asks it to act, and it is kept in the history as notes are. - Whoever may post a message may post a notice: every actor but an evaluator. Core checks the author of neither. A notice asks for less than a message, since it starts no turn, so there is nothing more to guard. Which rules may start a turn (D7) is decided by relayr's allowlist: a rule allowed to calls
Runner.send, and the others post notices. A person may post a notice too, to tell the thread something the agent should not act on.
Options considered¶
The mechanism:
| Option | Id space | On the wire | What branches |
|---|---|---|---|
A kind on PostMessage and MessagePosted (chosen) |
The message's, so ADR-0045 refuses a repeat as it stands | An optional field. A client that doesn't know it shows a notice as a message | Runner.execute's routing, the watcher's message case, evaluation's transcript, and change_notes, for the opt-in |
A post_notice command and a notice_posted event |
Its own, or the message's under another name | A new event type. A client that doesn't know it keeps a notice as an unknown event, and shows nothing | Nothing in the runner or the watcher, but two new union members, their needs and commit cases, a second type for relayr to bridge, and a second renderer in every client |
How an application that asks has its agent told:
| Option | Assessment |
|---|---|
| Change notes, when a run starts and while it runs (chosen) | Stored in the history as notes are, and the note says who posted it and that it is a notice |
| Only when a run starts | A notice posted during a turn is never told: the next turn's briefing starts after it |
Inserted into the loaded history by seq |
A notice could fall between a paused run's tool call and its result, which model APIs reject |
Consequences¶
- Easier: relayr posts a notice with
Workspace.as_actor(...).post_message(..., kind="notice"), and a retry with the same derived id is refused. A bridgedmessage_postedcarries itskind, so rules can leave notices out. - Easier: a client shows a notice by its
kind; one that doesn't know the field shows it as a message. Notegains a member without anartifact_id,NoticeNote, so code that handles every kind of note narrows it before reading one.- The
artifactr.messagescounter counts notices too, labelled withartifactr.message.kind. - Evaluation transcripts hold a thread's messages, not its notices. A transcript attributes each message by its author's role, so a rule's notice would read as a turn of the agent's, and a replayed thread would seed it into the agent's history.
Action items¶
-
kindonPostMessage,MessagePosted,Workspace.post_messageand MCP'spost_messagetool; the runner's routing, the watcher,NoticeNoteandArtifactWorkspace(notices=); the counter's label, and transcripts without notices; with conformance cases.