Skip to content

M2-10 Run document, patches and live box

No Date Ruling Reason
D-M2-10-0 2026-10-22 Built on the old integration line (stacked on M2-08 and M2-09) and ported to main after M2-09 landed: only the ticket's own commits were brought over. Changes to the German chapter files of the old base go to their English files on main (09-agents.md, ADR 0025), the rulings and changelog entries to fragments, and the gateway wiring from serve.go to wire/module_gateway.go; generated files were regenerated. The rules of the landing replace the integration branches; main is the reference.
D-M2-10-1 2026-10-21 Order of checks of Runs.Patch: load and authorize run.update, static checks, row lock, state, version, dynamic checks, write. An ended run gives 409 conflict with reason run_not_active before the version is compared (runs/app.Service.patchLocked). 04 15.5 and 09 "Stop" point 4 reject later patches without naming a code; with the state first an adapter after cancel_timeout learns that the run ended, not only that it is behind.
D-M2-10-2 2026-10-21 A stored patch clears stall_warned_at in the same UPDATE as the version step (query PatchRun). A patch is movement, so the hint "no movement since ..." of 09 5.4 must go.
D-M2-10-3 2026-10-21 errors[].code values of a patch: patch_path_forbidden also inside a value (the ordinal of a step), patch_target_missing, patch_target_exists, limit_reached; display texts give the textcheck rule name (invalid_utf8, control_character, bidi_control); last_text_excerpt and detail are agent output and not text checked (runs/domain.ParseOps, Apply). The closed code vocabulary of M1-18 has no code for these cases; agent output is treated like message content.
D-M2-10-4 2026-10-21 is_estimate of /cost must be true (invalid); plan item ids are unique within the plan (invalid). Every amount is an estimate until a reconciliation replaces it (10 12.1) and an adapter is no reconciliation; clients key plan items by id.
D-M2-10-5 2026-10-21 Set semantics and normalized ops: add and replace set a member also when it is absent, remove of an absent member is a no-op, no-ops are dropped, one stored patch is exactly one version step even when every op is a no-op; the values of the normalized ops are the stored values as getRun encodes them (runs/domain.Apply). Spec gap: 09 5.3 allows only replace on /model and /steps/<id>/finished_at, both start absent, and RFC 6902 section 4.3 requires the target of replace to exist.
D-M2-10-6 2026-10-21 Cutting: last_text_excerpt keeps its last run.excerpt_max_chars runes; a detail above run.step_detail_max_bytes gets its string leaves shortened longest first at a rune boundary and the member truncated: true, fallback {"truncated":true}; the result is deterministic (runs/domain.CutExcerpt, CutDetail). 03 only says "shortened" and 09 only "truncated"; a draft grows at the end, so the newest text stays visible.
D-M2-10-7 2026-10-21 Summary step: kind status, name steps_summarized, empty title, status completed, detail {"summarized": {"count", "by_kind"}}, a server UUIDv7 and the lowest removed ordinal. Only finished leaves are merged, never running steps or parents; without a candidate the first step add of the patch gives limit_reached (runs/domain/steps_summary.go). 03 names the summarization without its shape; parent_step_id cascades on delete, so a parent must stay.
D-M2-10-8 2026-10-21 Follow-up states go through the outgoing port runs/app.FollowupStates; internal/wire binds NoFollowups until M2-11, so every follow-up op gives patch_target_missing. run_followups is M2-11's table; the path rule and the value check are this ticket's.
D-M2-10-9 2026-10-21 Coalescing chain per run (gateway/domain.Chain): a change on the chain's version extends it (MergeOps), a higher base restarts it (a version step without patch, for example a state transition), a stale change is dropped. 04 11.3 point 3 names the merge of one window but not a version step without ops in between.
D-M2-10-10 2026-10-21 A connection whose last sent version of the run is not the chain's base gets the whole document as run.updated with snapshot instead of a gap frame. A gap frame would make every such client load getRun (04 11.3 point 4).
D-M2-10-11 2026-10-21 A frame dropped at a full outbound queue counts bothy.gateway.outbound.dropped with patch_replaced_by_snapshot and marks connection and run; the mark is retried at later window ends and every sync.run_patch_coalesce_interval until it is sent, the connection no longer focuses the thread or the run ended; never 4014. 04 11.3 point 3 and 7.2 point 6: an ephemeral frame never closes a connection, and the client must converge without getRun.
D-M2-10-12 2026-10-21 Oversize: a change published with Truncated, or an ops frame whose payload reaches sync.event_payload_max_bytes, goes out as the snapshot; a snapshot that reaches it goes out as {run_id, version} with envelope truncated: true and the client loads getRun. 04 2.2 rule 2 names the truncated envelope; the snapshot is the cheaper way back for the client.
D-M2-10-13 2026-10-21 Focus sends the document once per newly focused thread with an active run; resume counts the restored focus as new. While the connection catches up, the snapshot waits until it is live, because M1-66 buffers no ephemeral frame. The ephemeral events of a pause are lost (04 "Lifecycle" step 7); without a snapshot every client would load getRun on its first patch.
D-M2-10-14 2026-10-21 run.update answers nothing on success; a failure sends error {op, code, detail, ref} with detail <pointer>: <code> of the first field error or the reason, never a value. GatewayError has no current, so after base_version_conflict the adapter loads getRun (gateway/app.Gateway.runUpdate). asyncapi defines no reply and the adapter's next base is base_version + 1; spec gap: the error frame cannot carry the document.
D-M2-10-15 2026-10-21 The Confirmation of ADR 0025 is corrected from 422 to 400 validation_failed with errors[].code patch_path_forbidden. ADR 0015 (422 only for idempotency_key_reuse), 06 "Errors" point 4 and 09 AG-12 say so.
D-M2-10-16 2026-10-21 Names that differ from the brief: the domain input type is domain.RawOp (runs/domain may not import the port package), the stored step type is domain.DocStep (Step is the decision table of the trigger), the document DTO is runs.Run, the coalescer is gateway/app.Coalescer bound with Hub.BindLiveBox and run by Gateway.StartLiveBox, the snapshot encoder RunEncoder is injected by internal/wire (runs/adapters/httpapi.RunREST, one projection with getRun and the conflict current), runs/app.Service.BindPublisher binds the hub after the gateway is built, and Repository.PatchSteps reads step details only for the summary step. The brief predates the merged code (Boundaries); the code names win.
D-M2-10-17 2026-10-21 patchRun takes the op values from the request body as received (httpserver.RawJSONBody), and the gateway codec decodes the values of run.update raw too. The generated type folds JSON null and an absent value into nil, and the checks need the difference.
D-M2-10-18 2026-10-21 The independent RFC 6902 applier of the property tests is written in the test packages from RFC 6902 sections 4.1 to 4.3; no new dependency. Keeps go.mod and the license gate unchanged.
D-M2-10-19 2026-10-21 The end of a run (durable run.state_changed with an end state through Hub.Dispatch) drops the coalescer state of the run after its last window; a state transition raises the version without a patch, so the next patch restarts the chain and the viewers get a snapshot. Design note 9; no state may outlive an ended run.
D-M2-10-20 2026-10-21 The integration tests of the live box run on the full server of internal/wire (serve_livebox_test.go); the AG-12 red runs are recorded in the report. They need the gateway, REST and PostgreSQL together.
D-M2-10-21 2026-10-21 Numbers in a step detail are rewritten to their plain decimal form (no exponent, the scale of the literal, no sign on zero) before CutDetail measures the detail; when the plain forms alone exceed run.step_detail_max_bytes the stored value is {"truncated":true} (red team finding). A jsonb numeric prints its full decimal expansion, so a short literal like 1e-16000 turned a detail within the limit into megabytes on every read and 1e-20000 into a 500; measuring the printed form keeps the acceptance criterion "detail stays within its key".
D-M2-10-22 2026-10-21 Times in normalized ops (started_at, finished_at of a step) have the form getRun answers, a time.Time as JSON after the cut to milliseconds (RFC 3339, trailing zeros of the fraction dropped), and a window end sends nothing to a connection whose last sent version already reaches the window's newest version. Ops applied to a snapshot must give exactly the document of getRun; a fixed .000 form differed whenever the milliseconds ended in zero, and a retried snapshot of the newest version made the window end repeat it.