fix: always write model field to checkpoint metadata.json#882
fix: always write model field to checkpoint metadata.json#882
Conversation
The model field was tagged omitempty, causing it to be silently absent when the agent didn't provide model info. Consumers couldn't distinguish "unknown model" from "field not supported yet". Now the field is always present (empty string when unknown). Also wires up model reporting for Cursor SessionStart (was available but ignored) and Vogon (test agent), so all agents now report model info. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Ensures the LLM model identifier is consistently captured and persisted in checkpoint session metadata, so downstream consumers can rely on the model field always being present.
Changes:
- Always emit
modelin checkpoint sessionmetadata.jsonby removingomitemptyfromCommittedMetadata.Model. - Plumb Cursor hook
modelinto the normalizedSessionStartlifecycle event. - Add
modelto Vogon E2E hook payloads and extend E2E metadata parsing to include the field.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| e2e/vogon/main.go | Includes a deterministic model value in Vogon’s emitted hook payloads so canary E2E exercises model propagation. |
| e2e/testutil/metadata.go | Adds Model to the E2E SessionMetadata struct to read/validate the new persisted field. |
| cmd/entire/cli/checkpoint/checkpoint.go | Makes CommittedMetadata.Model always serialize to JSON (even when empty). |
| cmd/entire/cli/agent/vogon/hooks.go | Wires parsed hook model into Vogon lifecycle events (SessionStart/TurnStart/TurnEnd/SessionEnd). |
| cmd/entire/cli/agent/cursor/lifecycle.go | Wires Cursor SessionStart hook model into the normalized event. |
| Type: agent.SessionStart, | ||
| SessionID: raw.ConversationID, | ||
| SessionRef: raw.TranscriptPath, | ||
| Model: raw.Model, | ||
| Timestamp: time.Now(), |
There was a problem hiding this comment.
parseSessionStart now populates Event.Model, but there is no unit test asserting that SessionStart hooks propagate the model (Cursor tests currently only cover model on TurnStart). Adding a focused test would help prevent regressions, especially since headless Cursor runs rely on SessionStart/SessionEnd hooks.
| SessionID: raw.ConversationID, | ||
| SessionRef: raw.TranscriptPath, | ||
| Model: raw.Model, | ||
| Timestamp: time.Now(), |
There was a problem hiding this comment.
Cursor CLI hooks commonly send transcript_path: null (unmarshals to empty string). parseSessionStart currently forwards raw.TranscriptPath directly, unlike TurnStart/TurnEnd/SessionEnd which resolve the transcript path dynamically. Consider passing ctx into parseSessionStart and using resolveTranscriptRef(ctx, raw.ConversationID, raw.TranscriptPath) so SessionStart has a consistent, non-empty SessionRef in CLI mode.
Claude Code's parseTurnEnd and parseSessionEnd were ignoring the model field already available in sessionInfoRaw. Codex's parseTurnStart and parseTurnEnd similarly had model available but unused. Wire them up so model info propagates on every hook event, not just SessionStart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address Copilot review comment: add unit tests asserting that model fields are propagated through hook event parsing for all affected agents. - Claude Code: TurnEnd and SessionEnd model propagation - Cursor: SessionStart model propagation (with and without model) - Codex: TurnStart and TurnEnd model assertions on existing tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sessionEndRaw includes model but parseSessionEnd wasn't forwarding it, leaving the final model name unpersisted when a Cursor session ends without a useful Stop event. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
omitemptyfromCommittedMetadata.ModelJSON tag so the field is always present in metadata.json (empty string when unknown, never missing)parseTurnEndandparseSessionEndwere ignoring the model field already present insessionInfoRawparseTurnStartandparseTurnEndhad model available in raw types but unusedparseSessionStartwas ignoringraw.Modeldespite the field being parsedModelfield to E2ESessionMetadatatest structAll agents now report model info where feasible:
Test plan
mise run fmt— no formatting issuesmise run lint— 0 issuesmise run test— all unit tests passmise run test:e2e:canary— all 4 canary tests pass🤖 Generated with Claude Code