Context
"Keep only the last N items while counting the total, then unwind the ring in chronological order" is implemented twice. The total % limit index arithmetic is exactly the kind of code where an off-by-one slips in during unrelated edits.
Current state (verified locations)
internal/preview/preview.go:344-357 (ring write in loadMessages) + preview.go:373-387 (collectRing)
internal/codex/codex.go around lines 366-387 (appendMessage / collectMessages) — same pattern; read it first and note any boundary differences (total < limit, limit <= 0).
Task
- Add a small generic helper, suggested location
internal/ringtail/ringtail.go:
type Ring[T any] struct { buf []T; total int }
func New[T any](limit int) *Ring[T] // limit <= 0: define and document the behavior (match current callers)
func (r *Ring[T]) Add(v T)
func (r *Ring[T]) Collect() []T // chronological order, at most limit items
func (r *Ring[T]) Total() int
- Before migrating, write table-driven + property-style tests for the helper itself: total==0, total<limit, total==limit, total==limit+1, total==2*limit+3, limit==1. Property:
Collect() equals the last min(total, limit) items of the full input sequence.
- Migrate
internal/preview.loadMessages and the codex equivalent to the helper. If codex handles a boundary differently, keep its current behavior and document the difference in the test.
- Delete
collectRing/collectMessages and their now-redundant private tests (keep any that assert caller-level behavior).
Acceptance criteria
- One ring implementation; both callers use it.
go test ./... passes, including existing preview property tests.
go test -bench=. ./internal/preview ./internal/codex shows no regression (±5%).
Context
"Keep only the last N items while counting the total, then unwind the ring in chronological order" is implemented twice. The
total % limitindex arithmetic is exactly the kind of code where an off-by-one slips in during unrelated edits.Current state (verified locations)
internal/preview/preview.go:344-357(ring write inloadMessages) +preview.go:373-387(collectRing)internal/codex/codex.goaround lines 366-387 (appendMessage/collectMessages) — same pattern; read it first and note any boundary differences (total < limit,limit <= 0).Task
internal/ringtail/ringtail.go:Collect()equals the lastmin(total, limit)items of the full input sequence.internal/preview.loadMessagesand the codex equivalent to the helper. If codex handles a boundary differently, keep its current behavior and document the difference in the test.collectRing/collectMessagesand their now-redundant private tests (keep any that assert caller-level behavior).Acceptance criteria
go test ./...passes, including existing preview property tests.go test -bench=. ./internal/preview ./internal/codexshows no regression (±5%).