Context
internal/source/codex.go, internal/source/grok.go, and internal/source/opencode.go are near-identical wrappers whose only real job is delegating to the backend store and stamping s.Source = name. Adding a fifth backend means copy-pasting ~65 lines again.
Current state
See internal/source/codex.go (the whole file, 65 lines) — Scan/ScanFiltered call stamp(ss, name), FindByID/FindByLocator set s.Source = name, GrepKeys delegates directly, Messages delegates to the store. grok.go and opencode.go follow the same shape. The genuinely backend-specific parts are only:
- the store constructor (
codex.Open() etc.)
ResumeSpec (different binary/args per backend)
- whether
FindByLocator exists and how the locator decodes (file path for codex/grok; check what opencode does — it is SQLite-backed and may not implement LocatorFinder)
- the store method names (
MessagesForSession vs Messages — verify each)
Task
- Read all four wrapper files (
claude.go, codex.go, grok.go, opencode.go) and list the exact method-set differences first. Do not assume the shapes match; verify.
- Define an unexported interface in
internal/source, e.g.:
type sessionStore interface {
Scan() ([]*session.Session, error)
ScanFiltered(allow map[string]struct{}) ([]*session.Session, error)
FindByID(id string) (*session.Session, error)
GrepKeys(query string, regex bool) (map[string]struct{}, error)
}
- Implement one
storeSource struct { name string; store sessionStore; resumeSpec func(*session.Session) (string, []string, error) } that covers the shared methods (including the stamp / s.Source = name logic in exactly one place).
- Keep locator support as a separate optional wrapper (e.g.
fileLocatorSource embedding storeSource) so source.LocatorFinder is only satisfied by backends that actually support it — this mirrors the existing optional-interface design (internal/source/source.go:32).
- Where store method names differ (e.g.
MessagesForSession vs Messages), prefer renaming the store method for uniformity over adding adapter closures, unless the rename ripples too far.
claude.go may stay separate if it doesn't fit the store shape — do not force it.
Acceptance criteria
internal/source/{codex,grok,opencode}.go shrink to a constructor + ResumeSpec definition each (roughly ≤20 lines).
internal/source/source_test.go passes unchanged (it table-tests forName for all backends).
Source stamping behavior identical: ccsession list --json --source=all rows carry the correct source field (add/extend a test if none asserts this).
go test ./..., go vet ./..., golangci-lint run pass.
Context
internal/source/codex.go,internal/source/grok.go, andinternal/source/opencode.goare near-identical wrappers whose only real job is delegating to the backend store and stampings.Source = name. Adding a fifth backend means copy-pasting ~65 lines again.Current state
See
internal/source/codex.go(the whole file, 65 lines) —Scan/ScanFilteredcallstamp(ss, name),FindByID/FindByLocatorsets.Source = name,GrepKeysdelegates directly,Messagesdelegates to the store.grok.goandopencode.gofollow the same shape. The genuinely backend-specific parts are only:codex.Open()etc.)ResumeSpec(different binary/args per backend)FindByLocatorexists and how the locator decodes (file path for codex/grok; check what opencode does — it is SQLite-backed and may not implementLocatorFinder)MessagesForSessionvsMessages— verify each)Task
claude.go,codex.go,grok.go,opencode.go) and list the exact method-set differences first. Do not assume the shapes match; verify.internal/source, e.g.:storeSource struct { name string; store sessionStore; resumeSpec func(*session.Session) (string, []string, error) }that covers the shared methods (including thestamp/s.Source = namelogic in exactly one place).fileLocatorSourceembeddingstoreSource) sosource.LocatorFinderis only satisfied by backends that actually support it — this mirrors the existing optional-interface design (internal/source/source.go:32).MessagesForSessionvsMessages), prefer renaming the store method for uniformity over adding adapter closures, unless the rename ripples too far.claude.gomay stay separate if it doesn't fit the store shape — do not force it.Acceptance criteria
internal/source/{codex,grok,opencode}.goshrink to a constructor +ResumeSpecdefinition each (roughly ≤20 lines).internal/source/source_test.gopasses unchanged (it table-testsforNamefor all backends).Sourcestamping behavior identical:ccsession list --json --source=allrows carry the correctsourcefield (add/extend a test if none asserts this).go test ./...,go vet ./...,golangci-lint runpass.