Skip to content

M1-66 Gateway focus and typing

No Date Ruling Reason
M1-66/0 2026-10-17 Base int/m1-wave38 at 297ab0c, branch feat/M1-66-gateway-focus-typing in its own worktree; the branch is not merged locally and not pushed, the integration lane ports it. Brief, spec and contract were read from origin/main; the contract of focus, typing.start, typing.stop and typing.started (asyncapi, openapi, actions.yaml) is the same on the base apart from the translation, so no contract commit was brought in. Instructions of the launcher, which override the brief's merge step.
M1-66/1 2026-10-17 A focus with an empty set succeeds without an authorize check: the handler marks the op as naming no resource, and the dispatcher's proof accepts that marker. Every other successful op still needs a check of its action. An empty set grants nothing and only clears the focus; there is no thread to authorize, and closing the connection with 4000 would break the documented way to clear the focus.
M1-66/2 2026-10-17 focus and the typing ops authorize the thread by its id first and read its space and channel from chat afterwards. Unknown and unreadable threads both get not_found from authorize; a thread that disappears between the check and the read is treated as unknown. 02 "404 instead of 403": one code path for both. authz resolves the path itself, so the check needs no data from chat; chat only supplies the scope of typing.started and of the focus entry.
M1-66/3 2026-10-17 The rate limit of focus and the typing ops runs in the handler, after the dispatcher decoded the body; a malformed body closes with 4002 even when the bucket is empty. The dispatcher of M1-64 decodes before every handler; the order only differs for frames that close the connection anyway.
M1-66/4 2026-10-17 The rate-limit classes of the M1-66 ops are constants in internal/wire (GatewayOpClasses), held against x-rate-limit-class of asyncapi by TestGatewayOpRateClasses; the route generator stays unchanged. heartbeat and reauth carry rl.gateway_connect in the contract but must not consume it (05 section 3), so a generic "every op counts its class" would be wrong; only the three new ops count their class.
M1-66/5 2026-10-17 The register of ephemeral types holds the two focus-bound types typing.started and run.updated; the other ephemeral types of 05 enter it with the tickets that build them (M2-20, M3-23, M2, M6-15). Their candidate rules differ (one connection of the command owner, push.deliver with R-AZ-30) and are decided there; an entry now would claim a rule nobody built.
M1-66/6 2026-10-17 A failed admission check of an ephemeral frame drops the frame for that connection; it neither closes the connection nor retries. A failed re-check of a focused thread drops the thread. Ephemeral events are at most once and carry no seq, so no cursor can move past them; the durable path of the next event closes a revoked connection. Dropping a thread fails closed; the client focuses it again.
M1-66/7 2026-10-17 A focus entry keeps the space and channel of its thread from the time of the check; channel.archived and space.archived re-check the entries of that channel or space, thread.moved the moved thread, which takes the new channel and space of the event. Pruning changes the hub index only; the session's stored focus is re-checked on resume anyway. The hub has no structure of its own; the event names the affected channel or space, and the moved thread's new place is the scope of thread.moved.
M1-66/8 2026-10-17 occurred_at of an ephemeral event is written in UTC with milliseconds, the form of expires_at, so expires_at - occurred_at is exactly sync.typing_ttl for the client. Durable events keep their form. The brief asks for milliseconds in expires_at; with nanoseconds in occurred_at the difference would not be the TTL.
M1-66/9 2026-10-17 When the tests of the connection pass their own delivery (no hub), wire gives the gateway a publisher that relays nothing. The connection tests need no ephemeral delivery; production always builds the hub.
M1-66/10 2026-10-18 Port onto main: code and tests as in the ticket range (297ab0c..0a60ee0), which entered the integration as one squash without a conflict, except the points named here. The integration fixes INT-W41/1 to INT-W41/3 are not ported: they adapt this ticket to the agent consumers of M2-07 (no ephemeral frame before the consumer's catchup.complete, lint fixes of the union, stats.subscribe instead of ack as the still disabled op), and M2-07 is not on main, so ack stays disabled and TestGatewayOpsEnableList keeps ack. The changelog takes only the entry of this ticket (the M6-02 and M6-01 entries of the base are not on main), the module README keeps the go-arch-lint sentence of main, and TestConfigSchemaBuilds names the English chapter files. architecture/config-schema.json and api/routes/meta.gen.go are regenerated. No migration. The base int/m1-wave38 held tickets that are not on main (for example M2-24, M2-26, M3-13, M3-18, M4-18, M6-01); the port keeps the behaviour of the ticket and adapts only to what main has.