Skip to content

Commit 6ff82e4

Browse files
committed
✨ feat(readiness-check): add --list-examples and subset refresh support
- Add --list-examples flag to list available fixture names - Support --refresh-examples=<name[,name...]> for selective refresh - Validate requested fixture names upfront before processing - Add test coverage for list output and subset rewrites ✨ feat(doctor): add --prune-stale-follow-health for explicit cleanup - Add --prune-stale-follow-health flag to remove stale snapshots - Report prune status and reason in text/JSON output - Preserve fresh snapshots when prune is requested - Add test coverage for prune behavior
1 parent dfbcceb commit 6ff82e4

10 files changed

Lines changed: 409 additions & 12 deletions

File tree

docs/go/maintainer/development-tracker.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,20 @@ Current blockers:
498498
- Blockers: none.
499499
- Next step: decide whether the refresh helper should remain a mode on `readiness-check`, or move to a separate maintainer script if the checked-in example catalog grows further.
500500

501+
### 2026-03-17 Session Update
502+
503+
- Completed: Made the readiness example helper scale a bit better as the fixture catalog grows. `scripts/readiness-check` now supports `--list-examples` plus `--refresh-examples=<name[,name...]>`, validates requested fixture names up front, and test coverage now checks list output plus subset-only rewrites instead of assuming every refresh touches the full catalog.
504+
- In progress: none.
505+
- Blockers: none.
506+
- Next step: decide whether the current list-and-subset workflow is enough for maintainers, or whether the example catalog should eventually move into a small manifest file with richer metadata.
507+
508+
### 2026-03-17 Session Update
509+
510+
- Completed: Added explicit stale follow-health pruning to `doctor`. Operators can now run `doctor --prune-stale-follow-health` to remove only stale `follow-imports.health.json` sidecars, and the doctor text/JSON report now surfaces whether a snapshot was pruned plus the prune reason. App coverage now verifies stale snapshots are removed only when requested and that fresh snapshots are preserved.
511+
- In progress: none.
512+
- Blockers: none.
513+
- Next step: decide whether explicit stale pruning is enough operator hygiene, or whether a later slice should add a broader import/follow cleanup surface for old checkpoints and retry artifacts too.
514+
501515
## Recommended Next Step
502516

503517
Recommended next implementation slice:

docs/go/maintainer/mcp-integration.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ If a deliberate readiness-output change makes those fixtures drift, refresh them
220220
go run ./scripts/readiness-check --refresh-examples
221221
```
222222

223+
If you only need one or two fixtures while iterating on a specific output shape, first list the available fixture names and then refresh just the subset you want:
224+
225+
```powershell
226+
go run ./scripts/readiness-check --list-examples
227+
go run ./scripts/readiness-check --refresh-examples=ci-json
228+
```
229+
223230
Then rerun `go test ./scripts/readiness-check -run TestReadinessExampleOutputsStayInSync` plus the normal repo checks so the updated fixtures are verified in read-only mode again.
224231

225232
That combined check now covers:

docs/go/operator/import-ingestion.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ Multi-input `follow-imports` returns one aggregate report with command-level wat
206206
- `follow-imports` also keeps cumulative `watch_poll_catchups` and `watch_poll_catchup_bytes` counters for the lifetime of the process. Once poll catchup happens at least three times in the same process, the report adds a `WARN_FOLLOW_IMPORTS_POLL_CATCHUP` warning so operators and automation can treat notify mode as degraded even if it never fully falls back.
207207
- Each emitted `follow-imports` report also refreshes a last-known health sidecar under the normal log directory. `codex-mem doctor` reads that snapshot so operators can inspect the most recent follow-mode watch health even after the long-lived process has already exited.
208208
- For continuous follow mode, `doctor` now marks that sidecar as stale when it has not been refreshed for roughly three poll intervals, with a minimum freshness window of 30 seconds. Stale follow health adds `WARN_FOLLOW_IMPORTS_HEALTH_STALE` so operators can distinguish a healthy last-known state from an old snapshot left behind by a stopped process.
209+
- If you want to clear only stale follow-health sidecars without touching fresh ones, run `codex-mem.exe doctor --prune-stale-follow-health`. The doctor report then tells you whether it actually removed a stale snapshot via `follow_imports_health_pruned` and `follow_imports_health_prune_reason`.
209210
- When multi-input follow mode shares `--failed-output` or `--failed-manifest` base paths, `codex-mem` derives per-input file names before adding the byte-range suffix so retry artifacts from different inputs do not overwrite each other.
210211
- Each event uses the same imported-note workflow as `memory_save_imported_note`.
211212
- Existing explicit memory wins over weaker imported duplicates in the same project.

internal/app/doctor.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import (
1313
)
1414

1515
type doctorOptions struct {
16-
JSON bool
16+
JSON bool
17+
PruneStaleFollowHealth bool
1718
}
1819

1920
type doctorReport struct {
@@ -92,6 +93,8 @@ type doctorLoggingReport struct {
9293
type doctorFollowReport struct {
9394
HealthFile string `json:"health_file"`
9495
HealthPresent bool `json:"health_present"`
96+
HealthPruned bool `json:"health_pruned"`
97+
HealthPruneReason string `json:"health_prune_reason,omitempty"`
9598
LastUpdatedAt *time.Time `json:"last_updated_at,omitempty"`
9699
Status string `json:"status,omitempty"`
97100
Source string `json:"source,omitempty"`
@@ -117,6 +120,7 @@ type doctorMCPReport struct {
117120

118121
const (
119122
doctorJSONFlag = "--json"
123+
doctorPruneStaleFollowHealthFlag = "--prune-stale-follow-health"
120124
stringNone = "none"
121125
doctorFollowHealthStaleMultiplier = 3
122126
doctorFollowHealthMinimumWindow = 30 * time.Second
@@ -130,18 +134,22 @@ func parseDoctorOptions(args []string) (doctorOptions, error) {
130134
continue
131135
case doctorJSONFlag:
132136
options.JSON = true
137+
case doctorPruneStaleFollowHealthFlag:
138+
options.PruneStaleFollowHealth = true
133139
default:
134140
return doctorOptions{}, fmt.Errorf("unknown doctor flag %q", arg)
135141
}
136142
}
137143
return options, nil
138144
}
139145

140-
func buildDoctorReport(cfg config.Config, runtime db.RuntimeDiagnostics, toolCount int, followHealth *followImportsHealthSnapshot) doctorReport {
146+
func buildDoctorReport(cfg config.Config, runtime db.RuntimeDiagnostics, toolCount int, followHealth *followImportsHealthSnapshot, healthPruned bool, healthPruneReason string) doctorReport {
141147
now := time.Now().UTC()
142148
followReport := doctorFollowReport{
143-
HealthFile: followImportsHealthPath(cfg.Meta.LogDir),
144-
HealthPresent: followHealth != nil,
149+
HealthFile: followImportsHealthPath(cfg.Meta.LogDir),
150+
HealthPresent: followHealth != nil,
151+
HealthPruned: healthPruned,
152+
HealthPruneReason: strings.TrimSpace(healthPruneReason),
145153
}
146154
if followHealth != nil {
147155
age, stale := evaluateFollowImportsHealthStaleness(*followHealth, now)
@@ -281,6 +289,8 @@ func formatDoctorReport(report doctorReport) string {
281289
fmt.Sprintf("log_stderr=%t", report.Logging.LogStderr),
282290
fmt.Sprintf("follow_imports_health_file=%s", report.Follow.HealthFile),
283291
fmt.Sprintf("follow_imports_health_present=%t", report.Follow.HealthPresent),
292+
fmt.Sprintf("follow_imports_health_pruned=%t", report.Follow.HealthPruned),
293+
fmt.Sprintf("follow_imports_health_prune_reason=%s", fallbackString(report.Follow.HealthPruneReason)),
284294
fmt.Sprintf("follow_imports_last_updated_at=%s", pointerTimeOrNone(report.Follow.LastUpdatedAt)),
285295
fmt.Sprintf("follow_imports_status=%s", fallbackString(report.Follow.Status)),
286296
fmt.Sprintf("follow_imports_source=%s", fallbackString(report.Follow.Source)),
@@ -367,3 +377,21 @@ func followImportsHealthStaleWarnings(_ followImportsHealthSnapshot, age time.Du
367377
Message: fmt.Sprintf("follow-imports health snapshot is stale at %s", age.Truncate(time.Second)),
368378
}}
369379
}
380+
381+
func loadDoctorFollowImportsHealth(logDir string, pruneStale bool, now time.Time) (*followImportsHealthSnapshot, bool, string, error) {
382+
followHealth, err := loadFollowImportsHealthSnapshot(logDir)
383+
if err != nil {
384+
return nil, false, "", err
385+
}
386+
if !pruneStale || followHealth == nil {
387+
return followHealth, false, "", nil
388+
}
389+
_, stale := evaluateFollowImportsHealthStaleness(*followHealth, now)
390+
if !stale {
391+
return followHealth, false, "", nil
392+
}
393+
if err := pruneFollowImportsHealthSnapshot(logDir); err != nil {
394+
return nil, false, "", err
395+
}
396+
return nil, true, "stale", nil
397+
}

internal/app/import_follow.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,17 @@ func loadFollowImportsHealthSnapshot(logDir string) (*followImportsHealthSnapsho
14031403
return &snapshot, nil
14041404
}
14051405

1406+
func pruneFollowImportsHealthSnapshot(logDir string) error {
1407+
path := followImportsHealthPath(logDir)
1408+
if err := os.Remove(path); err != nil {
1409+
if os.IsNotExist(err) {
1410+
return nil
1411+
}
1412+
return fmt.Errorf("remove follow-imports health snapshot: %w", err)
1413+
}
1414+
return nil
1415+
}
1416+
14061417
func newFollowImportsHealthSnapshotFromReport(report followImportsReport, options followImportsOptions) followImportsHealthSnapshot {
14071418
return followImportsHealthSnapshot{
14081419
Status: report.Status,

internal/app/run.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"log/slog"
88
"os"
9+
"time"
910

1011
"codex-mem/internal/buildinfo"
1112
"codex-mem/internal/config"
@@ -63,7 +64,7 @@ func Run(ctx context.Context, cfg config.Config, args []string, stdin io.Reader,
6364
if err != nil {
6465
return err
6566
}
66-
followHealth, err := loadFollowImportsHealthSnapshot(cfg.Meta.LogDir)
67+
followHealth, healthPruned, healthPruneReason, err := loadDoctorFollowImportsHealth(cfg.Meta.LogDir, options.PruneStaleFollowHealth, time.Now().UTC())
6768
if err != nil {
6869
return err
6970
}
@@ -75,8 +76,10 @@ func Run(ctx context.Context, cfg config.Config, args []string, stdin io.Reader,
7576
"required_schema_ok", runtimeDiagnostics.RequiredSchemaOK,
7677
"fts_ready", runtimeDiagnostics.FTSReady,
7778
"json", options.JSON,
79+
"prune_stale_follow_health", options.PruneStaleFollowHealth,
80+
"follow_health_pruned", healthPruned,
7881
)
79-
report := buildDoctorReport(cfg, runtimeDiagnostics, mcp.ToolCount(), followHealth)
82+
report := buildDoctorReport(cfg, runtimeDiagnostics, mcp.ToolCount(), followHealth, healthPruned, healthPruneReason)
8083
output := formatDoctorReport(report)
8184
if options.JSON {
8285
output, err = formatDoctorReportJSON(report)

internal/app/run_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7+
"os"
78
"path/filepath"
89
"strings"
910
"testing"
@@ -195,6 +196,8 @@ func TestRunDoctorFlagsStaleFollowImportsHealthSnapshot(t *testing.T) {
195196
output := stdout.String()
196197
for _, fragment := range []string{
197198
"follow_imports_health_present=true",
199+
"follow_imports_health_pruned=false",
200+
"follow_imports_health_prune_reason=none",
198201
"follow_imports_health_stale=true",
199202
"follow_imports_warnings=1",
200203
"follow_imports_warning_1_code=WARN_FOLLOW_IMPORTS_HEALTH_STALE",
@@ -205,6 +208,125 @@ func TestRunDoctorFlagsStaleFollowImportsHealthSnapshot(t *testing.T) {
205208
}
206209
}
207210

211+
func TestRunDoctorPrunesStaleFollowImportsHealthSnapshotWhenRequested(t *testing.T) {
212+
root := t.TempDir()
213+
cfg := config.Config{
214+
File: config.FileConfig{
215+
DatabasePath: filepath.Join(root, "data", "codex-mem.db"),
216+
DefaultSystemName: "codex-mem",
217+
SQLiteDriver: "sqlite",
218+
BusyTimeout: 5 * time.Second,
219+
JournalMode: "WAL",
220+
LogFilePath: filepath.Join(root, "logs", "codex-mem.log"),
221+
LogMaxSizeMB: 20,
222+
LogMaxBackups: 10,
223+
LogMaxAgeDays: 30,
224+
LogCompress: true,
225+
LogAlsoStderr: false,
226+
},
227+
Meta: config.LoadMetadata{
228+
ConfigDir: filepath.Join(root, "configs"),
229+
ConfigFilePath: filepath.Join(root, "configs", "codex-mem.json"),
230+
LogDir: filepath.Join(root, "logs"),
231+
},
232+
}
233+
234+
snapshot := followImportsHealthSnapshot{
235+
Status: "ok",
236+
UpdatedAt: time.Now().UTC().Add(-2 * time.Minute),
237+
Source: "watcher_import",
238+
InputCount: 1,
239+
Continuous: true,
240+
PollIntervalSeconds: 5,
241+
RequestedWatchMode: "auto",
242+
ActiveWatchMode: "notify",
243+
}
244+
if err := saveFollowImportsHealthSnapshot(cfg.Meta.LogDir, snapshot); err != nil {
245+
t.Fatalf("saveFollowImportsHealthSnapshot: %v", err)
246+
}
247+
248+
var stdout bytes.Buffer
249+
if err := Run(context.Background(), cfg, []string{"doctor", "--prune-stale-follow-health"}, strings.NewReader(""), &stdout); err != nil {
250+
t.Fatalf("Run doctor --prune-stale-follow-health: %v", err)
251+
}
252+
253+
output := stdout.String()
254+
for _, fragment := range []string{
255+
"follow_imports_health_present=false",
256+
"follow_imports_health_pruned=true",
257+
"follow_imports_health_prune_reason=stale",
258+
"follow_imports_health_stale=false",
259+
"follow_imports_warnings=0",
260+
} {
261+
if !strings.Contains(output, fragment) {
262+
t.Fatalf("doctor output missing %q:\n%s", fragment, output)
263+
}
264+
}
265+
266+
if _, err := os.Stat(followImportsHealthPath(cfg.Meta.LogDir)); !os.IsNotExist(err) {
267+
t.Fatalf("expected stale follow health snapshot to be removed, stat err=%v", err)
268+
}
269+
}
270+
271+
func TestRunDoctorDoesNotPruneFreshFollowImportsHealthSnapshot(t *testing.T) {
272+
root := t.TempDir()
273+
cfg := config.Config{
274+
File: config.FileConfig{
275+
DatabasePath: filepath.Join(root, "data", "codex-mem.db"),
276+
DefaultSystemName: "codex-mem",
277+
SQLiteDriver: "sqlite",
278+
BusyTimeout: 5 * time.Second,
279+
JournalMode: "WAL",
280+
LogFilePath: filepath.Join(root, "logs", "codex-mem.log"),
281+
LogMaxSizeMB: 20,
282+
LogMaxBackups: 10,
283+
LogMaxAgeDays: 30,
284+
LogCompress: true,
285+
LogAlsoStderr: false,
286+
},
287+
Meta: config.LoadMetadata{
288+
ConfigDir: filepath.Join(root, "configs"),
289+
ConfigFilePath: filepath.Join(root, "configs", "codex-mem.json"),
290+
LogDir: filepath.Join(root, "logs"),
291+
},
292+
}
293+
294+
snapshot := followImportsHealthSnapshot{
295+
Status: "partial",
296+
UpdatedAt: time.Now().UTC(),
297+
Source: "watcher_import",
298+
InputCount: 1,
299+
Continuous: true,
300+
PollIntervalSeconds: 5,
301+
RequestedWatchMode: "auto",
302+
ActiveWatchMode: "notify",
303+
}
304+
if err := saveFollowImportsHealthSnapshot(cfg.Meta.LogDir, snapshot); err != nil {
305+
t.Fatalf("saveFollowImportsHealthSnapshot: %v", err)
306+
}
307+
308+
var stdout bytes.Buffer
309+
if err := Run(context.Background(), cfg, []string{"doctor", "--prune-stale-follow-health"}, strings.NewReader(""), &stdout); err != nil {
310+
t.Fatalf("Run doctor --prune-stale-follow-health: %v", err)
311+
}
312+
313+
output := stdout.String()
314+
for _, fragment := range []string{
315+
"follow_imports_health_present=true",
316+
"follow_imports_health_pruned=false",
317+
"follow_imports_health_prune_reason=none",
318+
"follow_imports_health_stale=false",
319+
} {
320+
if !strings.Contains(output, fragment) {
321+
t.Fatalf("doctor output missing %q:\n%s", fragment, output)
322+
}
323+
}
324+
325+
if _, err := os.Stat(followImportsHealthPath(cfg.Meta.LogDir)); err != nil {
326+
t.Fatalf("expected fresh follow health snapshot to remain, stat err=%v", err)
327+
}
328+
}
329+
208330
func TestRunDoctorReportsMissingConfigFileAsNone(t *testing.T) {
209331
root := t.TempDir()
210332
cfg := config.Config{
@@ -483,3 +605,13 @@ func TestRunDoctorRejectsUnknownFlag(t *testing.T) {
483605
t.Fatalf("unexpected error: %v", err)
484606
}
485607
}
608+
609+
func TestParseDoctorOptionsEnablesPruneStaleFollowHealth(t *testing.T) {
610+
options, err := parseDoctorOptions([]string{"--prune-stale-follow-health"})
611+
if err != nil {
612+
t.Fatalf("parseDoctorOptions: %v", err)
613+
}
614+
if !options.PruneStaleFollowHealth {
615+
t.Fatal("expected prune-stale-follow-health option to be enabled")
616+
}
617+
}

0 commit comments

Comments
 (0)