Feature: 073-activity-size-retention | Spec: spec.md | Plan: plan.md
Approach: Test-driven (tests written before implementation).
- T001 Confirm branch
073-activity-size-retentionis checked out andgo build ./...is green before starting.
- T002 Add
ActivityMaxSizeMB intfield withjson:"activity_max_size_mb,omitempty"andmapstructure:"activity-max-size-mb"to the config struct ininternal/config/config.go, next to the existingActivity*fields. - T003 Set the default
ActivityMaxSizeMB: 256inDefaultConfig()ininternal/config/config.go(alongsideActivityMaxRecords: 100000).
Goal: A size cap removes oldest records until the log is within budget. Independent test: seed a bucket over budget, prune to size, assert remaining bytes ≤ budget and oldest were removed.
- T004 [P] [US1] Write failing test
TestPruneActivitiesToSize_RemovesOldestUntilUnderBudgetininternal/storage/activity_size_test.go: save N activity records with known payload sizes (oldest→newest timestamps), callPruneActivitiesToSize(budget), assert summed remaining bytes ≤ budget and the removed records are the oldest. - T005 [US1] Implement
func (m *Manager) PruneActivitiesToSize(maxBytes int64) (int, error)ininternal/storage/activity.go: in oneUpdatetxn, forward-cursor theactivity_recordsbucket computing totallen(k)+len(v); if total ≤ maxBytes return 0; else delete oldest-first, subtracting each from total, stopping when total ≤ maxBytes; never delete the final (newest) remaining key. Return count deleted. GuardmaxBytes <= 0→ no-op returns (0, nil). - T006 [P] [US1] Write test
TestPruneActivitiesToSize_AlreadyUnderBudget_NoOpininternal/storage/activity_size_test.go: log under budget → returns 0, all records intact.
Goal: deletion never removes the newest record, even if a single record exceeds the budget. Independent test: one record larger than the budget → that newest record survives.
- T007 [P] [US3] Write test
TestPruneActivitiesToSize_KeepsNewestEvenIfOverBudgetininternal/storage/activity_size_test.go: seed records where the newest single record exceedsmaxBytes; assert the newest record is retained and older ones removed (log not emptied).
Goal: cap of 0 disables size pruning; larger cap retains more.
Independent test: PruneActivitiesToSize(0) deletes nothing.
- T008 [P] [US2] Write test
TestPruneActivitiesToSize_DisabledWhenZeroOrNegativeininternal/storage/activity_size_test.go:PruneActivitiesToSize(0)and(-1)return (0, nil) with all records intact.
- T009 [US1] Plumb the size budget into
ActivityService: add amaxSizeBytes int64field set fromActivityMaxSizeMB * 1024 * 1024where the service is constructed, ininternal/runtime/activity_service.go(find the constructor that already readsmaxAge/maxRecords). - T010 [US1] In
runRetentionCleanup(internal/runtime/activity_service.go), after the age and count prunes, callPruneActivitiesToSize(s.maxSizeBytes)whens.maxSizeBytes > 0; on success with deleted > 0 logInfo("Pruned activity records to size budget", deleted, max_size_mb); on error logError. - T011 [P] [US1] Write/extend a runtime test in
internal/runtime/activity_service_test.go(TestActivityRetention_SizeCapRemovesOldest): construct the service with a smallmaxSizeBytes, seed an oversized log, runrunRetentionCleanup, assert the log is within budget and newest records remain.
- T012 [P] Document
activity_max_size_mb(default 256, 0 = disabled, runs with age/count caps) indocs/configuration.mdand the activity/retention docs. - T013 Run the full local verification:
go test ./internal/storage/ ./internal/runtime/ ./internal/config/andgo build ./cmd/mcpproxy; ensure green. - T014 Run
gofmt/goimportsand./scripts/run-linter.shon changed files.
- Phase 2 (config field) blocks T009 (wiring reads the field).
- T005 (implementation) unblocks all storage tests passing; tests T004/T006/T007/T008 are written first (TDD) and fail until T005.
- Phase 6 (wiring) depends on T005 + Phase 2.
- T013/T014 last.
- T004, T006, T007, T008 are all in the same new test file — write together, but they only pass after T005.
- T012 (docs) is independent and can be done anytime after the config field exists.
User Story 1 (T002–T006 + T009–T011) delivers the core: a default-on size cap that bounds the activity log. US2/US3 are small additions to the same method.