fix(dispatch): sort local dispatch candidates for stable output · Entire
fix(dispatch): sort local dispatch candidates for stable output
18e5753→main·
alishakawaguchi·yesterday·2 files·+43 added/-0 removed
Trail review: the trailer fallback pass ranges over a map (reachableCheckpointIDs), and Go randomizes map iteration order, so candidates were appended in a non-deterministic order. Nothing downstream re-sorts, so re-running dispatch --local over the same window could emit bullets in a different order — and thus a different LLM-authored summary — each time.
Sort the collected candidates most-recent-first (tiebreak by checkpoint ID) before returning, so dispatch output is stable across runs.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01Wi6WfsYgVrWDrZXpu62u8u
Changes
2
cmd/entire/cli/dispatch
Mmode_local.go+16
Mmode_local_test.go+27
272 unmodified lines
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
272 unmodified lines
seen[idStr] = struct{}{}
}
// The second pass ranges over a map (randomized iteration order), so sort
// before returning to keep bullet order — and therefore the LLM-authored
// summary — stable across runs. Newest first, with the checkpoint ID as a
// deterministic tiebreak for equal timestamps.
sortCandidatesByRecency(candidates)
return candidates, nil
}
// sortCandidatesByRecency orders candidates most-recent-first by CreatedAt,
// breaking ties by checkpoint ID so the order is fully deterministic.
func sortCandidatesByRecency(candidates []candidate) {
sort.SliceStable(candidates, func(i, j int) bool {
if !candidates[i].CreatedAt.Equal(candidates[j].CreatedAt) {
return candidates[i].CreatedAt.After(candidates[j].CreatedAt)
}
return candidates[i].CheckpointID < candidates[j].CheckpointID
})
}
// readLocalSummaryTitle returns the latest session's outcome (falling back to
// its intent) for use as a bullet title, or "" when no session summary is
// available.
Mcmd/entire/cli/dispatch/mode_local.go+16
879 unmodified lines
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
879 unmodified lines
}
}
// TestSortCandidatesByRecency covers the trail-review finding: because the
// trailer fallback pass ranges over a map, candidates must be sorted before
// returning so dispatch output is stable across runs. Newest first, ties broken
// by checkpoint ID.
func TestSortCandidatesByRecency(t *testing.T) {
t.Parallel()
base := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
candidates := []candidate{
{CheckpointID: "ccc", CreatedAt: base},
{CheckpointID: "aaa", CreatedAt: base.Add(2 * time.Hour)},
{CheckpointID: "bbb", CreatedAt: base}, // same time as ccc → tiebreak by ID
{CheckpointID: "zzz", CreatedAt: base.Add(time.Hour)},
}
sortCandidatesByRecency(candidates)
got := make([]string, len(candidates))
for i, c := range candidates {
got[i] = c.CheckpointID
}
want := []string{"aaa", "zzz", "bbb", "ccc"} // newest first; bbb < ccc for the tie
for i := range want {
if got[i] != want[i] {
t.Fatalf("sortCandidatesByRecency order = %v, want %v", got, want)
}
}
}
func TestLoadCommitSubjectsByCheckpoint_UsesSingleWindowedLogScan(t *testing.T) {
tmpDir := t.TempDir()
argsFile := filepath.Join(tmpDir, "git-args.txt")