Trace loses scalar status/goal history, keeps only the first value
Problem
Gitlab::DuoWorkflow::ChannelValuesReconstructor#channel_changes (in ee/lib/gitlab/duo_workflow/channel_values_reconstructor.rb) reports only the first value of every scalar channel (status, goal). It drops every later transition.
The method's docstring says: "Each recorded change is one entry, so plan and status changes stay visible instead of collapsing to the final value." The real behavior is worse than what that sentence guards against. The trace collapses to the first value, not the final one.
Why it happens
step_action on a checkpoint blob tells Rails whether a blob is an append or a replace. conversation means append, compaction means replace.
The AI gateway sets this field in _serialize_channel_blobs, in duo_workflow_service/checkpointer/gitlab_workflow.py (around line 272). The variable starts as "compaction" and only switches to "conversation" inside two branches: list-versus-list appends and dict-versus-dict appends.
status is a WorkflowStatusEnum (a string) and goal is str | None. Neither is a list or a dict, so neither branch ever runs for them. Every scalar change gets written as step_action: "compaction".
#channel_changes treats a compaction blob mid-stream as an internal re-seed and skips it. It keeps only the blob at index 0. Every scalar transition after the first gets dropped.
plan is partly affected too. It's a dict, so _dict_of_list_delta handles it. But a step-status edit mutates an existing step in place instead of appending. That produces a non-append delta, so step_action stays "compaction" and the edit collapses the same way. Pure step appends still work.
Reproduction
Run ChannelValuesReconstructor#channel_changes against three status blobs, all with step_action: "compaction", holding these values in order:
"Not Started""Running""Completed"
The result is ["Not Started"]. The two later transitions are gone.
Why the specs pass
The test helper make_blob at ee/spec/models/ai/duo_workflows/workflow_spec.rb:1685 hardcodes step_action: 'conversation'. The trace spec at line 2080 asserts against status blobs the gateway can never actually produce. No spec uses a status blob with step_action: "compaction", so the bug has no coverage.
Scope
#channel_changes feeds Ai::DuoWorkflows::Workflow#full_trace_channel_values, the session trace artifact. This path sits behind the dw_read_blobs_trace feature flag, which has never been enabled on GitLab.com. There's no production impact today, but the flag can't be turned on until this is fixed.
This is out of scope for !251388 (merged). That MR changes the same file, but for a different defect (messages dropped at a compaction boundary). Fixing scalars isn't a one-line change: recording every compaction blob as a real change would also record every gateway-restart re-seed, since a restart re-seeds each channel as a full snapshot. The fix needs to skip consecutive duplicate values, or find another way to tell a real transition from a re-seed.
Acceptance criteria
- The trace reports every scalar transition for
statusandgoal, not just the first value. - A gateway-restart re-seed (a full snapshot re-write of an unchanged value) does not add a duplicate entry to the trace.
- The
planstep-edit case (an in-place edit to an existing step, not an append) is decided one way or the other, and the decision is documented. - A spec uses blob fixtures with
step_action: "compaction"for scalar changes, matching what the gateway actually writes.
Found while reviewing !251388 (merged) for #619495 (closed).