@@ -88,15 +88,11 @@ func (s *ManualCommitStrategy) getCheckpointLog(ctx context.Context, checkpointI
8888// sessionAttrData captures per-session data needed to compute combined attribution
8989// after the condensation loop has cleared state.PromptAttributions.
9090type sessionAttrData struct {
91+ condenseOpts // Embedded: shadowRef, headTree, parentTree, repoDir, headCommitHash, parentCommitHash
92+
9193 promptAttributions []PromptAttribution
9294 filesTouched []string
93- shadowRef * plumbing.Reference
94- headTree * object.Tree
95- parentTree * object.Tree
96- repoDir string
9795 attrBase string
98- headCommit string
99- parentCommit string
10096}
10197
10298// computeCombinedAttribution merges PromptAttributions from all sessions sharing
@@ -125,10 +121,22 @@ func computeCombinedAttribution(
125121 }
126122
127123 first := sessions [0 ]
124+ attrBase := first .attrBase
125+ for i := 1 ; i < len (sessions ); i ++ {
126+ if sessions [i ].attrBase != attrBase {
127+ logging .Warn (logging .WithComponent (ctx , "attribution" ),
128+ "combined attribution: sessions have divergent attribution base commits; using first session's base" ,
129+ slog .String ("first_base" , attrBase ),
130+ slog .String ("divergent_base" , sessions [i ].attrBase ),
131+ slog .Int ("session_index" , i ),
132+ )
133+ break
134+ }
135+ }
128136 syntheticState := & SessionState {
129137 PromptAttributions : merged ,
130- AttributionBaseCommit : first . attrBase ,
131- BaseCommit : first . attrBase ,
138+ AttributionBaseCommit : attrBase ,
139+ BaseCommit : attrBase ,
132140 }
133141 syntheticData := & ExtractedSessionData {
134142 FilesTouched : allFilesTouched ,
@@ -139,19 +147,53 @@ func computeCombinedAttribution(
139147 parentTree : first .parentTree ,
140148 repoDir : first .repoDir ,
141149 attributionBaseCommit : first .attrBase ,
142- headCommitHash : first .headCommit ,
143- parentCommitHash : first .parentCommit ,
150+ headCommitHash : first .headCommitHash ,
151+ parentCommitHash : first .parentCommitHash ,
144152 })
145153}
146154
155+ // resolveCommitContext extracts HEAD tree, parent tree, and parent commit hash
156+ // from a commit object. Best-effort: warns on failure and leaves fields nil/empty.
157+ // Both parentTree and parentCommitHash are set together or not at all, so callers
158+ // can rely on them being in sync.
159+ func resolveCommitContext (ctx context.Context , commit * object.Commit ) (headTree , parentTree * object.Tree , parentCommitHash string ) {
160+ logCtx := logging .WithComponent (ctx , "checkpoint" )
161+
162+ if t , err := commit .Tree (); err != nil {
163+ logging .Warn (logCtx , "failed to resolve HEAD tree; attribution will be skipped" ,
164+ slog .String ("commit" , commit .Hash .String ()),
165+ slog .String ("error" , err .Error ()))
166+ } else {
167+ headTree = t
168+ }
169+
170+ if commit .NumParents () > 0 {
171+ rawHash := commit .ParentHashes [0 ]
172+ if parent , err := commit .Parent (0 ); err != nil {
173+ logging .Warn (logCtx , "failed to load parent commit; parent-scoped attribution unavailable" ,
174+ slog .String ("parent_hash" , rawHash .String ()),
175+ slog .String ("error" , err .Error ()))
176+ } else if t , err := parent .Tree (); err != nil {
177+ logging .Warn (logCtx , "failed to load parent tree; parent-scoped attribution unavailable" ,
178+ slog .String ("parent_hash" , rawHash .String ()),
179+ slog .String ("error" , err .Error ()))
180+ } else {
181+ parentTree = t
182+ parentCommitHash = rawHash .String ()
183+ }
184+ }
185+
186+ return headTree , parentTree , parentCommitHash
187+ }
188+
147189// condenseOpts provides pre-resolved git objects to avoid redundant reads.
148190type condenseOpts struct {
149191 shadowRef * plumbing.Reference // Pre-resolved shadow branch ref (nil = resolve from repo)
150192 headTree * object.Tree // Pre-resolved HEAD tree (passed through to calculateSessionAttributions)
151193 repoDir string // Repository worktree path for git CLI commands
152194 headCommitHash string // HEAD commit hash (passed through for attribution)
153- parentTree * object.Tree // HEAD's first parent tree (nil iff parentCommitHash is empty)
154- parentCommitHash string // HEAD's first parent hash (empty iff parentTree is nil — initial commit or resolution failure)
195+ parentTree * object.Tree // HEAD's first parent tree (nil when parentCommitHash is empty or parent resolution failed )
196+ parentCommitHash string // HEAD's first parent hash (empty when parentTree is nil — initial commit or resolution failure)
155197}
156198
157199// CondenseSession condenses a session's shadow branch to permanent storage.
@@ -912,31 +954,17 @@ func (s *ManualCommitStrategy) CondenseSessionByID(ctx context.Context, sessionI
912954 if repoDir , rdErr := paths .WorktreeRoot (ctx ); rdErr == nil {
913955 headCommitOpts .repoDir = repoDir
914956 }
915- if headRef , hrErr := repo .Head (); hrErr == nil {
957+ if headRef , hrErr := repo .Head (); hrErr != nil {
958+ logging .Warn (logCtx , "condense-by-id: failed to resolve HEAD; attribution context unavailable" ,
959+ slog .String ("error" , hrErr .Error ()))
960+ } else {
916961 headCommitOpts .headCommitHash = headRef .Hash ().String ()
917- if headCommit , hcErr := repo .CommitObject (headRef .Hash ()); hcErr == nil {
918- if t , tErr := headCommit .Tree (); tErr != nil {
919- logging .Warn (logCtx , "condense-by-id: failed to resolve HEAD tree; attribution will be skipped" ,
920- slog .String ("commit" , headRef .Hash ().String ()),
921- slog .String ("error" , tErr .Error ()))
922- } else {
923- headCommitOpts .headTree = t
924- }
925- if headCommit .NumParents () > 0 {
926- rawHash := headCommit .ParentHashes [0 ]
927- if parent , pErr := headCommit .Parent (0 ); pErr != nil {
928- logging .Warn (logCtx , "condense-by-id: failed to load parent commit; parent-scoped attribution unavailable" ,
929- slog .String ("parent_hash" , rawHash .String ()),
930- slog .String ("error" , pErr .Error ()))
931- } else if t , tErr := parent .Tree (); tErr != nil {
932- logging .Warn (logCtx , "condense-by-id: failed to load parent tree; parent-scoped attribution unavailable" ,
933- slog .String ("parent_hash" , rawHash .String ()),
934- slog .String ("error" , tErr .Error ()))
935- } else {
936- headCommitOpts .parentTree = t
937- headCommitOpts .parentCommitHash = rawHash .String ()
938- }
939- }
962+ if headCommit , hcErr := repo .CommitObject (headRef .Hash ()); hcErr != nil {
963+ logging .Warn (logCtx , "condense-by-id: failed to load HEAD commit object" ,
964+ slog .String ("commit" , headRef .Hash ().String ()),
965+ slog .String ("error" , hcErr .Error ()))
966+ } else {
967+ headCommitOpts .headTree , headCommitOpts .parentTree , headCommitOpts .parentCommitHash = resolveCommitContext (ctx , headCommit )
940968 }
941969 }
942970
0 commit comments