Skip to content

M1-63 Catch-up over REST and snapshot

No Date Ruling Reason
M1-63/0 2026-10-15 Base int/m1-wave25 at 719cfaa, branch feat/M1-63-sync-catch-up in its own worktree; the branch is not merged locally and not pushed, the integration lane ports it. The brief, spec and contract were read from origin/main; the base already holds what the ticket needs (the query parameter epoch on listEvents, the required features on Snapshot, sync -> identity in 02 and architecture/modules.yaml, R-SY-03, R-SY-04, R-SY-16, R-SY-18), so no contract commit was brought in. Instructions of the launcher, which override the brief's merge step.
M1-63/1 2026-10-15 head_seq is the committed value of event_seq, read without a lock through the new instance.Versions.EventSeq; min_retained_seq is min(seq), or head_seq + 1 for an empty log. After retention the log can be empty while the counter is not, so max(seq) would report cursor_ahead wrongly; instance.Counters refuses calls outside the phase hooks (M1-15). A commit is visible before its counter lock is released (04 section 3.2).
M1-63/2 2026-10-15 The resync check order, first match wins: an epoch different from sync_epoch gives epoch_changed, then cursor > head_seq gives cursor_ahead, then cursor < min_retained_seq - 1 gives retention, then head_seq - cursor > sync.catchup_max_events gives too_far_behind. Without epoch there is no epoch check. A new time line can overtake old cursors, so the epoch decides first (R-SY-18); cursor_ahead stays the second net for a client without a stored epoch (R-SY-04); a lost range is reported as retention before the size of the backlog.
M1-63/3 2026-10-15 active_runs is empty (adapters/noruns, bound by internal/wire) until M2-05 binds the runs port, and every agent of agents has state: unknown without reported_billing_mode until heartbeats exist (M2-20). A caller the agent listing refuses as a whole (403, an app without the agents scope) gets an empty agents. The presence part is ended by D-M2-20-4 (M2-20): the snapshot shows the presence of the tracker. The table runs and the heartbeats do not exist in M1; AgentRuntimeState documents unknown. A snapshot must not fail for an app because it may not read agents.
M1-63/4 2026-10-15 The unread counters are computed through ports, never by a join across module tables: chat.Threads.Counters (new, threads of the listed channels with last_message_seq > unread_floor_seq), the person's read_states of those channels (new query ListReadStatesInChannels) and messaging.Mentions.CountUnread with the position GREATEST(last_read_seq, unread_floor_seq) per thread; unread is last_message_seq minus that position, never below 0. A thread is listed when unread or its mentions are above 0 and thread.read allows it in filter mode. Threads without a message above their floor are not listed, and their mentions do not count. The counters cover the channels the snapshot lists (visible, not archived). 02 "Rules" point 1 and 07 section 7.4 rule 6; every message of such a thread is at or below the floor, so nothing there is unread for anybody; every channel_id of unread_threads then names an entry of channels.
M1-63/5 2026-10-15 trace_context is in a catch-up envelope only when the caller is an agent; no delivery-only member is set in M1. 04 "Envelope": the traceparent goes only to agent adapters; invocation_token and redelivered come with M2.
M1-63/6 2026-10-15 The port is named Snapshot (02) and its DTO SnapshotView; the DTO and its parts (SnapshotSpace, SnapshotChannel, UnreadThread, RunSummary, AgentPresence, DraftRef, EvaluatedFlag, SnapshotPrincipal) are types of the sync port that mirror the OpenAPI schemas. Go cannot give an interface and a struct the same name in one package; the port package may import only kernel packages (M1-16/4), so it cannot carry the views of chat.
M1-63/7 2026-10-15 is_owner comes from identity.Directory.User (an active owners row) for people and is false for agents and apps; permission_version comes from Authorizer.Scope. authorize.Scope has no owner flag: InstanceWide is also true for a grant on the instance, which is no owner role.
M1-63/8 2026-10-15 CatchUp.Check authorizes events.catch_up on the instance; CatchUp.Page does not and checks each event with its receive action. The visible sets of the page query come from Authorizer.Scope with channel.read. The gateway reuses Check (M1-64, M1-65), so REST and WebSocket apply the same rule, including events:subscribe for apps; channel.read is the list action whose scope is the set of readable channels and spaces.
M1-63/9 2026-10-15 authorize.ResourceFromEvent(kind, id, space, channel, thread) (new in kernel/authorize, tests in internal/authz) names a thread, channel or space by its id without hints and gives any other kind only its most specific scope (thread, else channel, else space). The scope of a stored event is a copy from the time of the event; a thread moved since then would contradict a channel hint, which authz treats as an internal error. With the most specific hint authz reads the current path, so the rights at delivery decide (04 section 8.1).
M1-63/10 2026-10-15 channels lists the channels of ListSpaceChannels per visible space; a space that turns invisible or unknown between the two reads is skipped. DM channels without a space are not listed, because chat has no listing for them before direct messages (V3, dm.enabled). The brief asks for the listings of listSpaces and listSpaceChannels; M1 creates no DM channel.
M1-63/11 2026-10-15 Test code: test/messagingworld.NewSync builds the world of NewPersonal with catch-up and snapshot as internal/wire builds them (wire.NewSyncReads), with the identity and agents modules, the instance row with its sync_epoch (pgtest databases have none) and a configurable partition size; the retention case ages the first partition with UPDATE event_log SET occurred_at and runs the partition maintenance of M1-16 with the world's clock. AZ-P5 for the snapshot builds three random worlds through the public ports with a logged seed. The world of 07 section 11.1 in internal/authz is a fake world without HTTP; the shared HTTP world of chat and messaging has every module the snapshot reads.
M1-63/12 2026-10-15 Review finding (Blocker): pin.added and pin.removed (a message, received with thread.read), command.changed (a command, command.read on the channel) and grant.changed (a grant, grant.read on space, channel, host or principal) made every catch-up page that met them fail with 500. Fixed for the whole class in one place: authorize.ResourceFromEvent sets Resource.Event with the full scope, and authz (eventTarget, for Authorize and for webhook_out.deliver) decides such an event on the first object the receive action takes: the target of a grant (the subject principal of an instance grant), then the thread, channel and space of the scope; an event with no fitting object, or whose receive action is still deferred (file.read until M3), is filtered as not_found, never an error. The webhook delivery of apps builds its descriptor with ResourceFromEvent too. TestResourceFromEventCatalogue decides every durable event type of the catalogue without an error. A stored event that no rule can decide must not stop every reader at its seq; the fix in authz covers REST catch-up, the gateway (M1-65) and the webhooks, which had the same class.
M1-63/13 2026-10-15 Red team round 1 (NEIN, CI red): the test package api/routes lacked the two new operations in its stub server and route table, and golangci-lint and semgrep reported five findings in the new lines. Fixed without a behaviour change, apart from one: Page takes the receive action through authorize.ParseAction, and an event whose receive action is outside the catalogue of the build is filtered instead of failing the page. The semgrep rule allows catalogue ids only through constants or Parse functions; failing closed matches the webhook delivery.
M1-63/14 2026-10-18 Port onto main: code and tests as in the integration squash (with the M1-63 side of INT-W28/1) except the points named here. M2-26 is not on main, so authz has no authorizeDelivery and eventTarget runs only in Authorize, authorize.Resource gains only Event, and apps/app/deliver.go is not touched; the webhook parts of M1-63/12 apply when M2-26 is ported. The Identity of internal/wire on main has no Directory (it came with M2-24 and M3-13), so buildSyncReads and the messaging test world pass identityapp.NewDirectory over the identity repository, as chat and messaging do on main, and buildSyncReads needs only the agents and settings steps. The messaging test world on main builds no instance settings (M3-13), so NewSync builds them in syncReads for the identity writes, which have no Search. M3-18 and M1-62 are not on main: the chat step builds only the sync reads, threads.sql gains only ThreadCounters, and txn_registrations.golden is unchanged because the ticket registers no subscriber. The ticket keeps its behaviour; only collaborators that main does not have yet are left out or built the way main builds them.