Dedup incremental checkpoint blobs for redundant delta sends
Context
Incremental checkpoint blobs (p_duo_workflows_checkpoint_blobs) were introduced in gitlab-org/gitlab!239247 behind duo_workflow_incremental_checkpoints. The write path stores per-step channel deltas; current_thread groups them and step_action (conversation = append delta, compaction = full replacement) is the authoritative fold signal.
That MR does not dedup blobs, which is safe today because:
- The gateway sends each delta exactly once.
- The read/reconstruction fold path is not live yet (no
duo_workflow_read_incremental_checkpointsconsumer in Rails).
So duplicates can currently be neither produced nor misread.
Why we need dedup
We plan to send the same delta multiple times to add redundancy against failed requests (see Alper's thread on !239247). Once that lands, reconstruction must not double-apply a duplicated append delta.
The current unique index idx_duo_wf_checkpoint_blobs_unique on (project_id, workflow_id, thread_ts, channel, version, created_at) does not dedup retried sends, because created_at is set to Time.current per request (and created_at is forced into the key by the table being PARTITION BY RANGE (created_at)).
Proposed approach
Write-side (storage / idempotent insert):
- Make
created_atdeterministic per logical delta (e.g. derived fromthread_ts) so retries land in the same partition with an identical key. - Switch
bulk_insert!→insert_all(..., on_duplicate: :skip)keyed on(workflow_id, thread_ts, channel, version, step_action).
Read-side (correctness):
- Make the reconstruction fold idempotent: order by
version, dedup, and apply compaction-precedence — for a given(channel, version), acompaction(full value) overrides aconversation(tail delta).
Important caveat
Dedup must key on (channel, version, step_action) (or a content hash), not (channel, version) alone. The same version integer can legitimately be emitted with different data/step_action: a normal append emits step_action="conversation" with only the appended tail, while a force_rewrite (after a gateway restart that loses _prev_channel_values) re-emits the same version as step_action="compaction" with the full value. These are not interchangeable — see _serialize_channel_blobs in gitlab-ai-gateway.
Sequencing constraint
This is a safe follow-up only if it lands before either of these goes live:
- the gateway's redundant-send behaviour, and
- the read/reconstruction fold path (
duo_workflow_read_incremental_checkpoints).
Whichever ships first is the deadline.
Open question to confirm during implementation
Can a restart re-emit an already-persisted version as a compaction (vs. only ever emitting versions strictly greater than what's stored)? This determines whether the dual-representation collision is real or only theoretical, and thus whether read-side compaction-precedence is strictly required.