Read incremental checkpoint blobs (phase 2)
## Goal Serve Duo Workflow checkpoint `channel_values` by reconstructing them from **incremental blobs** instead of loading the full checkpoint header JSONB. This is the **read** half of the incremental-checkpoint work (parent epic [&22628](https://gitlab.com/groups/gitlab-org/-/work_items/22628)); the write path (phase 1) is tracked in [604684](https://gitlab.com/gitlab-org/gitlab/-/work_items/604684). The read machinery (`Gitlab::DuoWorkflow::ChannelValuesReconstructor` + `Ai::DuoWorkflows::Workflow` read methods) is shared; each **consumer** switches from the header read to blob reconstruction independently. ## Feature-flag model - **Master kill switch**: `duo_workflow_read_incremental_checkpoints` — disables reconstruction everywhere at once. Rollout issue [604687](https://gitlab.com/gitlab-org/gitlab/-/work_items/604687). - **Per-consumer flags**: each consumer is additionally gated by its own flag so it can be rolled out and rolled back independently. A consumer reconstructs from blobs only when **both** the kill switch **and** its own flag are on. ## Deliverables | Deliverable | Feature flag | Delivery issue | FF rollout issue | MR | |---|---|---|---|---| | Read engine + `Workflow` read methods + kill switch | `duo_workflow_read_incremental_checkpoints` (kill switch) | #604677 | #604687 | !247133 | | Internal checkpoint API / gateway (`by_thread_ts`) | `dw_read_blobs_api` | #606997 | #606987 | !247134 | | Trace download (`GET trace.jsonl`) | `dw_read_blobs_trace` | #606998 | #606988 | !247135 | | GraphQL presenter reads (`duoMessages`, `lastDuoMessage`, `executionStatus`, raw `checkpoint`/`compressedCheckpoint`) | `dw_read_blobs_graphql` | #607000 | #606989 | !243813 (partial) | | Messaging / notifications (`Checkpoint#ui_chat_log`: callback worker, progress reader, result email) | `dw_read_blobs_notifications` | #607001 | #606990 | — | GraphQL fields collapse to a single flag because they share the same presenter/query surface and cannot be rolled out independently of each other. ## Consumer inventory (source of truth) | Consumer | Reads | Where | Status | |---|---|---|---| | Internal checkpoint API / gateway | all channels | `workflows_internal.rb` GET checkpoint + `by_thread_ts` | migrated (247134) | | Trace download | ui_chat_log/plan/status | `workflows.rb` `trace_channel_values` | migrated (247135) | | Session messages GraphQL | ui_chat_log | `WorkflowCheckpointEventPresenter#duo_messages`/`#last_duo_message` | migrated (243813) | | `executionStatus` GraphQL | status | `workflow_event_type.rb` → presenter `execution_status` | not touched | | Raw checkpoint GraphQL + websocket | all (raw) | `workflow_event_type.rb` `checkpoint`/`compressedCheckpoint` + FE `workflow_utils.js` | not touched | | Messaging / notifications | ui_chat_log | `checkpoint.rb#ui_chat_log` → callback worker, progress reader, result email | not touched | | Goal backfill | `__start__.goal` | `create_checkpoint_service` — reads the checkpoint being written | N/A (not a blob read) |
epic