Context
With four backends (claude/opencode/grok/codex) the same small pure helpers have been copy-pasted across packages. Any behavior tweak (e.g. the "future timestamps sink to the bottom" rule) now has to be applied in up to 5 places.
Current state (verified locations)
sortEpoch — 5 copies:
internal/source/all.go:245
internal/codex/codex.go:473
internal/grok/grok.go:494
internal/opencode/scan.go:131
internal/session/scan.go:145 (this one takes *Session instead of int64; same rule)
pathIsDir — 4 copies: internal/codex/codex.go:492, internal/grok/grok.go:532, internal/opencode/scan.go:138, internal/session/parse.go:278
allowed — 3 copies: internal/codex/codex.go:480, internal/grok/grok.go:520, internal/opencode/scan.go:103
idCorruptsRow — 3 copies: internal/codex/codex.go:488, internal/grok/grok.go:528, internal/opencode/scan.go:99
firstNonEmpty — 2 copies: internal/grok/grok.go:511, internal/session/parse.go:220
- The
sort.Slice block using sortEpoch + "ID ascending tiebreak" is also duplicated next to each sortEpoch copy (e.g. internal/session/scan.go:131-138).
Task
- Add a new file
internal/session/helpers.go with exported versions:
func SortEpoch(epoch, nowEpoch int64) int64
func SortByRecency(out []*Session, nowEpoch int64) — encapsulates the full sort.Slice with the ID tiebreak
func PathIsDir(path string) bool
func Allowed(allow map[string]struct{}, id string) bool
func IDCorruptsRow(id string) bool
func FirstNonEmpty(values ...string) string
Copy the bodies from the existing implementations verbatim (they are identical); keep the existing doc comments, especially the future-timestamp rationale on sortEpoch.
- Replace every listed copy with a call to the shared function and delete the local definitions. All these packages already import
internal/session, so no import cycle is possible.
- Replace each duplicated
sort.Slice block with session.SortByRecency(...).
- Add a table-driven test file
internal/session/helpers_test.go covering each helper (include: future epoch clamps to 0, nil allow map, empty values for FirstNonEmpty, tab/newline cases for IDCorruptsRow). Move/merge any existing per-package tests of these helpers here and delete the duplicates.
Acceptance criteria
grep -rn "func sortEpoch\|func pathIsDir\|func allowed(\|func idCorruptsRow\|func firstNonEmpty" internal/ --include="*.go" returns only the new shared definitions (plus tests).
- No behavior change:
go test ./..., go vet ./..., golangci-lint run, gofmt -l . all pass/clean.
Context
With four backends (claude/opencode/grok/codex) the same small pure helpers have been copy-pasted across packages. Any behavior tweak (e.g. the "future timestamps sink to the bottom" rule) now has to be applied in up to 5 places.
Current state (verified locations)
sortEpoch— 5 copies:internal/source/all.go:245internal/codex/codex.go:473internal/grok/grok.go:494internal/opencode/scan.go:131internal/session/scan.go:145(this one takes*Sessioninstead ofint64; same rule)pathIsDir— 4 copies:internal/codex/codex.go:492,internal/grok/grok.go:532,internal/opencode/scan.go:138,internal/session/parse.go:278allowed— 3 copies:internal/codex/codex.go:480,internal/grok/grok.go:520,internal/opencode/scan.go:103idCorruptsRow— 3 copies:internal/codex/codex.go:488,internal/grok/grok.go:528,internal/opencode/scan.go:99firstNonEmpty— 2 copies:internal/grok/grok.go:511,internal/session/parse.go:220sort.Sliceblock usingsortEpoch+ "ID ascending tiebreak" is also duplicated next to eachsortEpochcopy (e.g.internal/session/scan.go:131-138).Task
internal/session/helpers.gowith exported versions:func SortEpoch(epoch, nowEpoch int64) int64func SortByRecency(out []*Session, nowEpoch int64)— encapsulates the fullsort.Slicewith the ID tiebreakfunc PathIsDir(path string) boolfunc Allowed(allow map[string]struct{}, id string) boolfunc IDCorruptsRow(id string) boolfunc FirstNonEmpty(values ...string) stringCopy the bodies from the existing implementations verbatim (they are identical); keep the existing doc comments, especially the future-timestamp rationale on
sortEpoch.internal/session, so no import cycle is possible.sort.Sliceblock withsession.SortByRecency(...).internal/session/helpers_test.gocovering each helper (include: future epoch clamps to 0, nil allow map, empty values for FirstNonEmpty, tab/newline cases for IDCorruptsRow). Move/merge any existing per-package tests of these helpers here and delete the duplicates.Acceptance criteria
grep -rn "func sortEpoch\|func pathIsDir\|func allowed(\|func idCorruptsRow\|func firstNonEmpty" internal/ --include="*.go"returns only the new shared definitions (plus tests).go test ./...,go vet ./...,golangci-lint run,gofmt -l .all pass/clean.