Add checkpoint_ns support to scope Duo Workflow checkpoints
What does this MR do and why?
Duo Workflow's DelegationNode dispatches nested subgraph invocations
(e.g. delegated subagents) that need their own checkpoint lineage,
separate from the flow's top-level checkpoints. Without a way to
distinguish them, callers could not fetch or scope checkpoints to one
specific nested invocation.
This adds checkpoint_ns to Checkpoint.earliest/.latest, the
presenter, the internal API (create and list endpoints), the
checkpoint entity, and the GraphQL first_checkpoint/latest_checkpoint
fields and DuoWorkflowEvent type. Blank/omitted checkpoint_ns
continues to refer to the session's own top-level lineage, preserving
prior behavior.
It also adds the migration for the checkpoint_ns column on
p_duo_workflows_checkpoints (the column/constraint were already
present in db/structure.sql, but the migration and its
db/schema_migrations marker were missing), and regenerates the
GraphQL introspection schemas and OpenAPI v3 docs that drifted out of
date from the new fields/params.
No new index was added for checkpoint_ns: every query filtering on
it (Checkpoint.earliest/.latest, the checkpoints list endpoint)
always scopes to a single workflow first via the workflow.checkpoints
association, which already uses the existing
index_p_duo_workflows_checkpoints_thread index on
(workflow_id, thread_ts). checkpoint_ns is then applied as a cheap
filter on that already-narrow, per-workflow row set.
Also batches the new checkpointWrites GraphQL field via
BatchLoader::GraphQL so resolving it across multiple workflows in one
request issues a single query instead of one per workflow.
Query and query plan for Checkpoint.earliest/.latest
Raw SQL for the common (for_checkpoint_ns(nil)) path:
SELECT "p_duo_workflows_checkpoints".* FROM "p_duo_workflows_checkpoints"
WHERE "p_duo_workflows_checkpoints"."workflow_id" = 2010
AND "p_duo_workflows_checkpoints"."created_at" <= '2026-07-14 18:35:36.846298'
AND "p_duo_workflows_checkpoints"."checkpoint_ns" IS NULL
ORDER BY "p_duo_workflows_checkpoints"."thread_ts" ASC LIMIT 1EXPLAIN (ANALYZE, BUFFERS) (top of plan; ran against my local dev
database — see caveat below):
Limit (cost=6.69..9.07 rows=1 width=398) (actual time=0.145..0.146 rows=0 loops=1)
Buffers: shared hit=56
-> Merge Append (cost=6.69..80.52 rows=31 width=398) (actual time=0.144..0.145 rows=0 loops=1)
Sort Key: p_duo_workflows_checkpoints.thread_ts
Buffers: shared hit=56
-> Index Scan using p_duo_workflows_checkpoints_20260614_workflow_id_thread_ts_idx on p_duo_workflows_checkpoints_20260614 ...
Index Cond: (workflow_id = 2010)
Filter: ((checkpoint_ns IS NULL) AND (created_at <= ...))
Buffers: shared hit=5
-> (repeats per daily partition within the retention window)
Planning Time: 7.920 ms
Execution Time: 2.259 msEvery partition uses the existing (workflow_id, thread_ts) index
(Index Cond: workflow_id = ?) and applies checkpoint_ns as a
post-index Filter, not a sequential scan.
Caveat: the plan above is from local dev data (~5.3k rows / 56 MB
across all partitions of p_duo_workflows_checkpoints), not
GitLab.com production data, so costs/row estimates aren't
representative at scale. The db:gitlabcom-database-testing pipeline
still needs to be triggered against production-like data before
requesting database review — I don't have access to trigger that job.
References
Screenshots or screen recordings
N/A — backend/API only change.
How to set up and validate locally
- Run
bundle exec rails db:migrateto apply thecheckpoint_nsmigration. bundle exec rspec ee/spec/models/ai/duo_workflows/checkpoint_spec.rb ee/spec/presenters/ai/duo_workflows/workflow_presenter_spec.rb ee/spec/requests/api/ai/duo_workflows/workflows_internal_spec.rb ee/spec/requests/api/graphql/ai/duo_workflows/workflows_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.