| D-M4-21-0 |
2026-10-24 |
Built on main after M4-20 and M2-31 landed; the changelog entry and these rulings are fragments. Differences to the brief's names, following the merged code: the run store is RunStore of sync_engine and the document RunDocument, focused through EngineFocus.focusRun (agents/providers.dart, EngineRunSource); the card body is agents/message_card.dart (MessageCard), the built-ins run through agents/commands/thread_command_runner.dart, the REST reads lie in agents/agent_reads.dart; mayStop and stopRun of M4-23 decide and send Stop; isOwnerProvider of M4-33 gives the owner flag of getMe; the session column's agent part is AgentSection with the provider-free AgentSectionView. |
D-W-100, D-W-114 (merged code wins over the brief's names). |
| D-M4-21-1 |
2026-10-24 |
The newest active run of the thread by queued_at gets the pinned box (R-DM-14 allows one; a late event may show two for a moment). The box is expanded by default; collapsing it shows the compact line, local state per run while the app runs (the binding is M4-24). The slot keeps one announcer for the thread's runs, passed to every box, and announces the final state when the box goes (the box of an active run never sees the end); the run time counts with a one-second timer, so it also counts with reduced motion. "Send again" shows only to the run's triggering person and sends once per press. The anchor message of an active run draws nothing; an ended run draws the collapsed line from its summary, the step counts of the summary count as tools until the document is loaded. A question waits for the run's triggering person ("Waiting for you" for that person, "Waiting for " for others). |
01 6.2, D3; 09 5.4; brief Design note 1. |
| D-M4-21-2 |
2026-10-24 |
Follow-ups are decided by the server: the composer's send path does not change during a run; it only reads "Follow-up to " while the thread has an active run, and the message's trigger_state shows followup_delivered from the event. The agent's line " is working" shows while its run is running (not queued, not waiting_input); typing events of agents are never read. |
09 7; 04 15.4; D-L9; brief Design note 2. |
| D-M4-21-3 |
2026-10-24 |
Pending interaction lock: a card with an interaction in the outbox that is not confirmed and not failed ignores further activations (also between the tap and the stored entry), and DsMessageComponents keeps its own lock until the card's state changes; a failed interaction shows as the card's error (problemText), not as a failed-write row, and the card is operable again while it is active. Whether the reader may use a card follows only the server's reports: the channel level trigger, the capabilities of the ChannelView, restricted_to with the run's triggered_by, and the owner flag of getMe (owners meet every rule); the server checks again. The values of InteractionCreate are {} for a button, {"selected": [...]} for a select and the field map for a form. |
09 11.2, 11.3; components.md 8; 01 6.5; brief Design note 3. |
| D-M4-21-4 |
2026-10-24 |
Autocomplete: a request goes out after ui.autocomplete_debounce without a newer input; when it goes out, the request still in flight is cancelled (CancelToken) and a late answer of an older request is dropped by a generation counter; the list shows only the server's choices. Command syntax in the composer: /name or /handle:name (09 10.3), the option values in order or as option:value, the last option takes the rest of the line. The built-ins come first in the suggestions, then the operator's commands, then the others; a name of a built-in is always the built-in, an unqualified name of another agent's command that clashes is reached only qualified (/handle:name also names the operator's command). A built-in the server's answers do not allow here is not offered and, when typed, is not sent at all: the text stays and the composer says it is not available here, because a reserved name sent as a message would reach the model or a running turn (09 10.1, 09 7). A refused invokeCommand belongs to the thread it was sent from, so it shows there as a failed write with its reason; the composer's autocomplete compares requests by value, so an answer or another rebuild never asks the agent again (Stop needs an active run the person may stop, /new and /resume the level trigger, /title the title right, /close write access in an open thread). |
09 10.1, 10.3, 10.5; components.md 9; brief Design note 4. |
| D-M4-21-5 |
2026-10-24 |
The commands of a channel load with listChannelCommands once per opened channel into the local commands table (later changes come as command.changed); the suggestions read the table filtered to the scopes of the channel (instance, its space, the channel; the most specific per owner and name). The session column's agent part reads getAgent, listAgentSessions, getThreadCosts, listThreadRuns (first page, "n+" with more) and the project binding of getChannelBehavior; a refused read leaves its row out. |
15 6.1; brief Scope and Design note 5. |
| D-M4-21-6 |
2026-10-24 |
The fake server: rest/agent_ui_handlers.dart and world_agents.dart (simplifications in the sync engine README); ScriptedAgent writes the live box anchor only with anchors: true and resolves @handle tokens only with world.resolveMentions, so the tests of earlier tickets keep their message sequences; the step AgentStep.until holds a script at a gate the test opens, so the integration tests observe each trigger state deterministically. The integration tests use a forum post, because a thread of a text channel is stored only through its starter line. |
Brief Scope "Fake server handlers"; rules for deterministic tests. |