| 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. |