-
Notifications
You must be signed in to change notification settings - Fork 540
Retain cached JSONL history within requested date ranges #60361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
988c342
079b892
a9707d7
e4b8bb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # ADR-60361: Retain Cached JSONL History Within Date Ranges | ||
|
|
||
| **Date**: 2026-09-12 | ||
| **Status**: Draft | ||
| **Deciders**: gh-aw maintainers | ||
|
|
||
| --- | ||
|
|
||
| ### Context | ||
|
|
||
| The `gh aw logs --cached-jsonl` flow reuses previously stored JSONL records and appends newly collected runs, but the PR description shows that cached run entries outside a requested `--start-date` / `--end-date` window were previously left in place. This made cached output inconsistent with the date-bounded query the user asked for, even though the command continued to preserve non-run metadata and append new results immediately. The diff adds post-collection filtering in both single-target and multi-target log orchestration, plus regression coverage for inclusive date boundaries and file-permission preservation. The architectural question is how cached JSONL state should behave when users request a bounded date range. | ||
|
|
||
| ### Decision | ||
|
|
||
| We will keep `--cached-jsonl` as an append-first cache format, then atomically filter cached run records to the resolved requested date range after collection completes. We decided to preserve non-run, unknown-schema, and undated records while pruning only dated run entries outside the inclusive bounds, because the PR evidence shows the cache must remain reusable and forward-compatible without returning stale run history for date-scoped requests. We will apply the same lifecycle in both single-target and multi-target log collection paths so date-range semantics stay consistent across command modes. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Leave all previously cached run records untouched | ||
|
|
||
| The existing behavior effectively preserved the entire JSONL history and only appended newly collected results. This was considered because it keeps cache writes simple and maximizes retained history. It was not chosen because the PR description and tests show that users asking for a specific date range should not keep seeing stale cached run entries outside that range. | ||
|
|
||
| #### Alternative 2: Rewrite the cache to contain only newly fetched records for the current invocation | ||
|
|
||
| Another option would be to discard prior cache contents and rebuild the JSONL file solely from the current run's collected records. This was considered because it guarantees strict alignment with the current query window. It was not chosen because the PR evidence explicitly preserves workflow-list, rate-limit, unknown, and undated records, and the existing cache design values incremental append/reuse rather than full replacement. | ||
|
|
||
| #### Alternative 3: Filter results only in memory and keep the on-disk cache broader than the requested range | ||
|
|
||
| The command could have filtered the final rendered output while leaving the backing JSONL file unchanged. This was considered because it avoids an extra read/filter/write pass on the cache file. It was not chosen because the reported bug is specifically about cached JSONL state retaining out-of-range run history, which would continue to affect later reuse and make the on-disk cache diverge from requested date-range semantics. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - Cached JSONL files now match inclusive `--start-date` and `--end-date` expectations for run records, reducing stale results during later reuse. | ||
| - The cache continues to preserve non-run metadata, unknown records, and undated records, maintaining forward compatibility and diagnostic usefulness. | ||
| - Single-target and multi-target logs flows share the same post-collection filtering lifecycle, reducing semantic drift between command modes. | ||
|
|
||
| #### Negative | ||
| - Each date-bounded cached run now incurs an additional full-file read and atomic rewrite step after collection, increasing I/O cost for large cache files. | ||
| - Cache lifecycle behavior becomes more complex because correctness depends on append-first collection followed by deferred pruning. | ||
| - Date-range correctness now depends on consistent date parsing and boundary resolution, creating more edge cases around timestamps and date-only end bounds. | ||
|
|
||
| #### Neutral | ||
| - The implementation keeps the JSONL file format unchanged and introduces behavior through orchestration and writer helpers rather than a new cache schema. | ||
| - Help text and flag descriptions now document that date-bounded runs prune cached run entries after collection while retaining other record kinds. | ||
| - Regression coverage explicitly checks preserved file permissions and retained non-run record types, clarifying the cache contract for future changes. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -364,6 +364,93 @@ func (w *cachedLogsJSONLWriter) appendRecord(record []byte) error { | |
| return nil | ||
| } | ||
|
|
||
| func (w *cachedLogsJSONLWriter) filterDateRange(startDate, endDate string) error { | ||
| if w == nil || (startDate == "" && endDate == "") { | ||
| return nil | ||
| } | ||
| dateRange, err := newCachedLogsJSONLDateRange(startDate, endDate) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| w.mu.Lock() | ||
| defer w.mu.Unlock() | ||
| data, err := os.ReadFile(w.path) | ||
| if errors.Is(err, os.ErrNotExist) { | ||
| return nil | ||
| } | ||
| if err != nil { | ||
| return fmt.Errorf("failed to read cached logs JSONL for date filtering: %w", err) | ||
| } | ||
| filtered := make([]byte, 0, len(data)) | ||
| for _, line := range bytes.SplitAfter(data, []byte{'\n'}) { | ||
| trimmed := bytes.TrimSpace(line) | ||
| if len(trimmed) == 0 { | ||
| filtered = append(filtered, line...) | ||
| continue | ||
| } | ||
| var record cachedLogsJSONLRecord | ||
| if err := json.Unmarshal(trimmed, &record); err != nil || | ||
| record.Kind != cachedLogsJSONLKindRun || | ||
| record.SchemaVersion != cachedLogsJSONLSchemaVersion || | ||
| record.Run == nil || | ||
| record.Run.CreatedAt.IsZero() { | ||
| filtered = append(filtered, line...) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] 💡 Suggested fixGuard on @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a9707d7: |
||
| continue | ||
| } | ||
| if !dateRange.includes(record.Run.CreatedAt) { | ||
| continue | ||
| } | ||
| filtered = append(filtered, line...) | ||
| } | ||
| if bytes.Equal(data, filtered) { | ||
| return nil | ||
| } | ||
| if err := writeFileAtomically(w.path, filtered); err != nil { | ||
| return fmt.Errorf("failed to filter cached logs JSONL by date range: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| type cachedLogsJSONLDateRange struct { | ||
| start time.Time | ||
| end time.Time | ||
| endIsDateOnly bool | ||
| } | ||
|
|
||
| func newCachedLogsJSONLDateRange(startDate, endDate string) (cachedLogsJSONLDateRange, error) { | ||
| resolvedStart, resolvedEnd, err := resolveLogsDateRange(startDate, endDate, time.Now()) | ||
| if err != nil { | ||
| return cachedLogsJSONLDateRange{}, err | ||
| } | ||
| dateRange := cachedLogsJSONLDateRange{endIsDateOnly: len(resolvedEnd) == len("2006-01-02")} | ||
| if resolvedStart != "" { | ||
| dateRange.start, err = parseFilterDate(resolvedStart) | ||
| if err != nil { | ||
| return cachedLogsJSONLDateRange{}, fmt.Errorf("failed to parse cached logs JSONL start date: %w", err) | ||
| } | ||
| } | ||
| if resolvedEnd != "" { | ||
| dateRange.end, err = parseFilterDate(resolvedEnd) | ||
| if err != nil { | ||
| return cachedLogsJSONLDateRange{}, fmt.Errorf("failed to parse cached logs JSONL end date: %w", err) | ||
| } | ||
| } | ||
| return dateRange, nil | ||
| } | ||
|
|
||
| func (r cachedLogsJSONLDateRange) includes(createdAt time.Time) bool { | ||
| if !r.start.IsZero() && createdAt.Before(r.start) { | ||
| return false | ||
| } | ||
| if r.end.IsZero() { | ||
| return true | ||
| } | ||
| if r.endIsDateOnly { | ||
| return createdAt.Before(r.end.AddDate(0, 0, 1)) | ||
| } | ||
| return !createdAt.After(r.end) | ||
| } | ||
|
|
||
| func (request cachedWorkflowRunsRequest) key() (string, error) { | ||
| data, err := json.Marshal(request) | ||
| if err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -152,8 +152,9 @@ Use --artifacts all to download all artifacts, or specify individual sets such a | |
| --artifacts agent,firewall to fetch only what you need. | ||
|
|
||
| Use --cached-jsonl to reuse matching run records without downloading and processing their | ||
| artifacts again. New results are appended immediately as JSON Lines. Aggregate analysis may be approximate when compact | ||
| cached records omit detailed data. | ||
| artifacts again. New results are appended immediately as JSON Lines. When a date range is specified, | ||
| cached run records outside that range are removed after collection; other record types are retained. | ||
|
Comment on lines
+155
to
+156
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a9707d7:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] This doc says cached run records outside the requested range are always removed, but 💡 Suggested fixEither (a) plumb @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a9707d7: |
||
| Aggregate analysis may be approximate when compact cached records omit detailed data. | ||
|
|
||
| All available artifact sets: %s. | ||
|
|
||
|
|
@@ -228,6 +229,8 @@ func loadStdinLogsOptions(cmd *cobra.Command) (StdinLogsOptions, error) { | |
| } | ||
| return StdinLogsOptions{ | ||
| OutputDir: values.OutputDir, | ||
| StartDate: values.StartDate, | ||
| EndDate: values.EndDate, | ||
| Engine: values.Engine, | ||
| Runtime: values.Runtime, | ||
| RepoOverride: values.RepoOverride, | ||
|
|
@@ -634,7 +637,7 @@ func addLogsCommandFlags(logsCmd *cobra.Command, validArtifactSets string) { | |
| logsCmd.Flags().String("drain3-weights", "", "Path to existing Drain3 weights JSON used to seed log pattern training") | ||
| logsCmd.Flags().String("format", "", "Output format: console (decorated tables), tsv (tab-separated), pretty (cross-run report), markdown (cross-run Markdown). Default: compact agent-optimized output") | ||
| logsCmd.Flags().String("report-file", "", "Write --format markdown output directly to this file path instead of stdout (creates parent directories as needed)") | ||
| logsCmd.Flags().String("cached-jsonl", "", "Path to cached logs JSONL to reuse for matching runs and append new results immediately") | ||
| logsCmd.Flags().String("cached-jsonl", "", "Path to cached logs JSONL to reuse, append new results, and retain runs in the requested date range") | ||
| logsCmd.Flags().Int("last", 0, "Alias for --count/-c: number of recent runs to download") | ||
| logsCmd.Flags().StringSlice("artifacts", []string{"info"}, "Artifact sets to download (default: info — compact workflow metadata). Use 'all' for everything, or comma-separate sets. Valid sets: "+validArtifactSets) | ||
| logsCmd.Flags().String("cache-before", "", "(Cache eviction) Evict locally cached run folders for runs before this date, prior to downloading. Accepts deltas like -1d, -1w, -1mo (or explicit day counts like -30d), or an absolute date YYYY-MM-DD. Unlike --start-date, this only clears local cache and does not filter which runs are fetched.") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 4e5...
filterDateRangenow requiresrecord.SchemaVersion == cachedLogsJSONLSchemaVersionfor run records, so unknown/future-schema run records are preserved. Added a regression test with aschema_version: 99run record in the excluded range that stays in the file.