feat: add cleanup-cache-memory job to agentics maintenance workflow#25908
feat: add cleanup-cache-memory job to agentics maintenance workflow#25908
Conversation
Add a new scheduled job that cleans outdated cache-memory caches: - Lists all caches with "memory-" prefix via GitHub Actions API - Groups caches by key prefix (everything before the run ID) - Sorts by embedded run ID (last numeric segment) - Keeps the latest entry per group, deletes the rest - Includes timeouts between API calls to avoid throttling - Skips if GitHub API rate limit is too low (< 100 remaining) - Checks rate limit periodically during deletion and stops early if needed Files added: - actions/setup/js/cleanup_cache_memory.cjs - cleanup script - actions/setup/js/cleanup_cache_memory.test.cjs - unit tests (20 tests) Files modified: - pkg/workflow/maintenance_workflow.go - add cleanup-cache-memory job - pkg/workflow/maintenance_workflow_test.go - include new job in condition checks - .github/workflows/agentics-maintenance.yml - regenerated Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5d9226f6-d004-4ef1-b72a-d5ba94d545d5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5d9226f6-d004-4ef1-b72a-d5ba94d545d5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
| * @param {number} ms - Milliseconds to wait | ||
| * @returns {Promise<void>} | ||
| */ | ||
| function delay(ms) { |
| * @param {any} github - GitHub REST client | ||
| * @returns {Promise<number>} Remaining requests, or -1 on error | ||
| */ | ||
| async function getRateLimitRemaining(github) { |
There was a problem hiding this comment.
@copilot refactor to rate limiting helper file
There was a problem hiding this comment.
Extracted to rate_limit_helpers.cjs in 56107ff. Exports getRateLimitRemaining, checkRateLimit, and MIN_RATE_LIMIT_REMAINING. Also integrates fetchAndLogRateLimit for observability logging. Includes 6 unit tests.
| * @param {string} key - Cache key string | ||
| * @returns {number | null} The extracted run ID, or null if not found | ||
| */ | ||
| function extractRunId(key) { |
| let page = 1; | ||
| const perPage = 100; | ||
|
|
||
| while (true) { |
There was a problem hiding this comment.
Added MAX_LIST_PAGES = 50 constant (allows up to 5000 caches) and changed the loop to while (page <= MAX_LIST_PAGES) in 56107ff. Logs a warning if the limit is reached.
| key: "memory-", | ||
| per_page: perPage, | ||
| page, | ||
| sort: "key", |
- Reuse delay from expired_entity_cleanup_helpers.cjs instead of duplicating - Extract rate limit checking to shared rate_limit_helpers.cjs with tests - Combine extractRunId/deriveGroupKey into single parseCacheKey function - Add MAX_LIST_PAGES (50) upper bound on pagination loop - Sort caches by last_accessed_at descending (newest first) - Add fetchAndLogRateLimit logging at start and end of cleanup - Add detailed logging: repository info, kept entries, page progress - Log rate limit remaining during periodic checks Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b4041e3a-bd6c-45f6-abaa-d48acfb07f3c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Added in 56107ff:
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot run on "clean_cache_memories" command |
Add clean_cache_memories as a new workflow_dispatch operation so the cleanup-cache-memory job can be triggered on demand. The job now runs on schedule, empty operation, or the clean_cache_memories operation. The run_operation job excludes this operation since it has its own dedicated job. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5f72e332-33fd-4059-985e-30fe470376b7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
| // Add cleanup-cache-memory job for scheduled runs and clean_cache_memories operation | ||
| // This job lists all caches starting with "memory-", groups them by key prefix, | ||
| // keeps the latest run ID per group, and deletes the rest. | ||
| yaml.WriteString(` |
There was a problem hiding this comment.
@copilot use expression builder to generate optimize if expressions
There was a problem hiding this comment.
Refactored in a83d229. The cleanup-cache-memory and run_operation if conditions are now built using composable expression builder functions (BuildAnd, BuildOr, BuildNotEquals, etc.) with RenderCondition optimizer. Added buildNotForkCondition, buildNotDispatchOrEmptyOperation, buildNotForkAndScheduledOrOperation, and buildRunOperationCondition helpers.
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5f72e332-33fd-4059-985e-30fe470376b7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Added Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Replace hand-written if expressions for cleanup-cache-memory and run_operation jobs with composable expression builder functions. Introduces buildNotForkCondition, buildNotDispatchOrEmptyOperation, buildNotForkAndScheduledOrOperation, and buildRunOperationCondition helpers that leverage RenderCondition with optimizer. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/564047d2-495a-4162-b2e2-6bc4f638dd34 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/564047d2-495a-4162-b2e2-6bc4f638dd34 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new scheduled (and on-demand) maintenance job to clean up outdated GitHub Actions caches created by cache-memory, plus shared rate-limit helper utilities and accompanying tests.
Changes:
- Adds
cleanup-cache-memoryjob generation + a newclean_cache_memoriesworkflow_dispatch operation. - Introduces
cleanup_cache_memory.cjscleanup script with unit tests covering parsing, grouping, pagination, rate-limit behavior, and error handling. - Adds shared
rate_limit_helpers.cjsutilities with dedicated unit tests and regenerates the maintenance workflow YAML.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/maintenance_workflow.go | Generates the new cleanup job and dispatch operation; refactors run_operation condition generation. |
| pkg/workflow/maintenance_workflow_test.go | Updates condition/operation choice assertions to include the new dedicated operation/job. |
| actions/setup/js/rate_limit_helpers.cjs | New shared rate limit checking utilities used by the cleanup script. |
| actions/setup/js/rate_limit_helpers.test.cjs | Unit tests for the new rate limit helpers. |
| actions/setup/js/cleanup_cache_memory.cjs | New cache listing/grouping/deletion script with throttling + rate-limit guardrails. |
| actions/setup/js/cleanup_cache_memory.test.cjs | Unit tests for cleanup behavior, pagination bounds, and logging. |
| .github/workflows/agentics-maintenance.yml | Regenerated workflow with the new cleanup-cache-memory job and operation choice. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 7/7 changed files
- Comments generated: 4
| async function getRateLimitRemaining(github, operation = "rate_limit_check") { | ||
| try { | ||
| await fetchAndLogRateLimit(github, operation); | ||
| const { data } = await github.rest.rateLimit.get(); | ||
| return data.rate.remaining; |
There was a problem hiding this comment.
getRateLimitRemaining() makes two rateLimit.get() API calls: one indirectly via fetchAndLogRateLimit() (which already calls github.rest.rateLimit.get()), then again immediately afterwards. This doubles rate-limit consumption per check and undermines the goal of being rate-limit conservative. Consider making a single rateLimit.get() call and using that response both for logging and for extracting data.rate.remaining (e.g., have fetchAndLogRateLimit return the response, or log from the same response without re-fetching).
| for (const cache of toDelete) { | ||
| // Check rate limit periodically (every 10 deletions) | ||
| if (deletedCount > 0 && deletedCount % 10 === 0) { |
There was a problem hiding this comment.
The periodic rate-limit check is keyed off deletedCount, so if deletions fail (and deletedCount doesn’t increment) the script may never re-check rate limits and could continue making API calls even as the limit drops. Track attempts/processed count (or use the loop index) for the modulo check instead of successful deletions.
| for (const cache of toDelete) { | |
| // Check rate limit periodically (every 10 deletions) | |
| if (deletedCount > 0 && deletedCount % 10 === 0) { | |
| for (let i = 0; i < toDelete.length; i++) { | |
| const cache = toDelete[i]; | |
| // Check rate limit periodically (every 10 processed caches) | |
| if (i > 0 && i % 10 === 0) { |
| * Lists all caches with "memory-" prefix, groups them by key prefix, | ||
| * keeps the latest run ID per group, and deletes the rest. | ||
| * Includes timeouts to avoid GitHub API throttling and skips | ||
| * if rate limiting is too high. |
There was a problem hiding this comment.
Docstring says "skips if rate limiting is too high" but the implementation skips when remaining rate limit is too low. Update the comment to match behavior to avoid confusion for future maintainers.
| * if rate limiting is too high. | |
| * if the remaining rate limit is too low. |
| */ | ||
| async function checkRateLimit(github, operation = "rate_limit_check") { | ||
| const remaining = await getRateLimitRemaining(github, operation); | ||
| if (remaining !== -1 && remaining < MIN_RATE_LIMIT_REMAINING) { |
There was a problem hiding this comment.
The checkRateLimit doc comment claims it "logs a warning" when the limit is too low, but the function currently only returns { ok: false } and does not emit any logs. Either adjust the comment or add the warning log here (so callers don’t need to duplicate messaging).
| if (remaining !== -1 && remaining < MIN_RATE_LIMIT_REMAINING) { | |
| if (remaining !== -1 && remaining < MIN_RATE_LIMIT_REMAINING) { | |
| console.warn( | |
| `[${operation}] Rate limit too low to continue: ${remaining} remaining (minimum ${MIN_RATE_LIMIT_REMAINING})` | |
| ); |
🏗️ Design Decision Gate — ADR RequiredThis PR makes significant changes to core business logic (
AI has analyzed the PR diff and generated a draft ADR to help you get started: 📄 Draft ADR: View draft ADR content# ADR-25908: Add Cache-Memory Cleanup as a Scheduled Maintenance Job
**Date**: 2026-04-12
**Status**: Draft
**Deciders**: Unknown (inferred from PR #25908 by Copilot / pelikhan)
---
## Part 1 — Narrative (Human-Friendly)
### Context
The cache-memory feature stores agent working memory in GitHub Actions caches using keys of the form `memory-{context}-{runID}`. Each workflow run creates a new cache entry for its context group, but old entries from prior runs are never automatically removed. Over time this accumulates stale cache entries, consuming repository cache quota and potentially slowing cache-list operations. A dedicated cleanup mechanism is needed to prune outdated entries while preserving the most recent state for each context group.
### Decision
We will add a `cleanup-cache-memory` job to the existing agentics maintenance workflow that runs on the same two-hour schedule as other maintenance tasks. The job lists all caches with the `memory-` prefix, groups them by key prefix (everything except the trailing run ID), keeps only the entry with the highest run ID per group, and deletes all older entries. Rate limit safety is enforced through a shared `rate_limit_helpers.cjs` module: the job skips entirely if available rate limit is below 100 requests, introduces 250 ms delays between deletions, 100 ms delays between pagination requests, re-checks the rate limit every 10 deletions, and caps list pagination at 50 pages (5,000 caches). The `clean_cache_memories` workflow_dispatch operation allows the job to be triggered on demand independently of other scheduled tasks.
### Alternatives Considered
#### Alternative 1: Event-Driven Cleanup on Each Workflow Run
Cache cleanup could be triggered as a post-job step each time a workflow run that uses cache-memory completes — deleting all entries for the context group except the one just written. This eliminates stale caches immediately and avoids accumulation. It was not chosen because it couples cleanup logic into every workflow that writes to cache-memory, significantly increasing maintenance surface area and complicating the workflows themselves. It also introduces a risk of race conditions if two concurrent runs for the same context group try to clean up simultaneously.
#### Alternative 2: Time-Based Expiry via GitHub Cache TTL
GitHub Actions caches that have not been accessed in 7 days are automatically evicted by GitHub's platform. Relying on this built-in TTL would require no custom code. It was not chosen because the 7-day window can allow a large accumulation of stale entries in active repositories, and there is no way to reduce the TTL below 7 days or differentiate "latest" from "outdated" entries within the platform's native eviction policy.
#### Alternative 3: Delete All Entries Except the Most Recently Accessed
Instead of using the run ID embedded in the cache key as the version discriminator, the job could rely on the API's `last_accessed_at` sort order and keep only the first result per group prefix. It was not chosen because `last_accessed_at` reflects read access, not write recency — a cache entry read by a failed re-run could appear more recent than the entry representing the latest successful state. Using the run ID as an explicit version is more deterministic.
### Consequences
#### Positive
- Repository cache quota is recovered automatically on a two-hour schedule without manual intervention.
- Rate limit risk is bounded: the job skips when headroom is low, delays between API calls, and stops early on exhaustion.
- The `clean_cache_memories` dispatch operation allows operators to trigger cleanup immediately after a bulk operation.
- The shared `rate_limit_helpers.cjs` module consolidates rate-limit checking logic previously reimplemented in each maintenance script.
#### Negative
- The run ID embedded in the cache key becomes a load-bearing convention: any future change to the key format that removes or repositions the numeric run ID suffix will silently break `parseCacheKey()` grouping.
- Caches whose keys contain no numeric segment are silently skipped, which may hide unexpected key formats.
- The 50-page / 5,000-cache pagination cap means repositories with unusually large numbers of `memory-` caches may not be fully cleaned in a single run.
#### Neutral
- The job is generated by `pkg/workflow/maintenance_workflow.go` using the same templating approach as other jobs, consistent with the existing code-generation pattern.
- New helper functions (`buildNotForkCondition`, `buildNotForkAndScheduledOrOperation`, `buildRunOperationCondition`) were factored out to keep condition-building DRY across all maintenance jobs.
- 27 unit tests cover the new script and 6 tests cover the shared rate limit helpers.What to do next
Why ADRs MatterADRs create a searchable, permanent record of why the codebase looks the way it does. The cache key convention ( 📋 Michael Nygard ADR Format ReferenceAn ADR must contain these four sections to be considered complete:
All ADRs are stored in
|
🧪 Test Quality Sentinel ReportTest Quality Score: 84/100✅ Excellent test quality
Test Classification Details📋 All 28 test cases (click to expand)
Flagged Tests — Requires Review
|
| Component | Score | Calculation |
|---|---|---|
| Behavioral Coverage (40 pts) | 39 | 27/28 design tests × 40 |
| Error/Edge Case Coverage (30 pts) | 15 | 14/28 tests with error/edge cases × 30 |
| Low Duplication (20 pts) | 20 | 0 duplicate clusters detected |
| Proportional Growth (10 pts) | 10 | All ratios < 2:1 (1.43×, 1.63×, 0.19×) |
| Total | 84 |
Verdict
✅ Check passed. 4% of new tests are implementation tests (threshold: 30%). The test suite demonstrates strong behavioral coverage — especially noteworthy is the comprehensive error-path and edge-case coverage in
cleanup_cache_memory.test.cjs: paginated fetches, partial delete failures, rate-limit exhaustion mid-run, and null/empty input guards. The one weak spot is a constant-value test inrate_limit_helpers.test.cjswhich can be improved but does not block merging.
📖 Understanding Test Classifications
Design Tests (High Value) verify what the system does:
- Assert on observable outputs, return values, or state changes
- Cover error paths and boundary conditions
- Would catch a behavioral regression if deleted
- Remain valid even after internal refactoring
Implementation Tests (Low Value) verify how the system does it:
- Assert on internal function calls (mocking internals)
- Only test the happy path with typical inputs
- Break during legitimate refactoring even when behavior is correct
- Give false assurance: they pass even when the system is wrong
Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.
🧪 Test quality analysis by Test Quality Sentinel · ● 1.2M · ◷
There was a problem hiding this comment.
✅ Test Quality Sentinel: 84/100. Test quality is excellent — 4% of new tests are implementation tests (threshold: 30%). The new JavaScript tests for cleanup_cache_memory and rate_limit_helpers show strong behavioral coverage including error paths, edge cases, and pagination guards. One minor improvement opportunity: replace the constant-value test MIN_RATE_LIMIT_REMAINING should be 100 with a boundary-behavior assertion.
Summary
Adds a new
cleanup-cache-memoryscheduled job to the agentics maintenance workflow that cleans out outdated caches created by cache-memory. The job can also be triggered on demand via theclean_cache_memoriesworkflow_dispatch operation.How it works
The new job runs on the same schedule as the existing maintenance tasks (every 2 hours) and can be manually triggered via the
clean_cache_memoriesoperation:memory-prefix via the GitHub Actions cache API, sorted newest-first bylast_accessed_atparseCacheKey()which extracts both the group key and run ID in a single passRate limit safety
rate_limit_helpers.cjsand skips entirely if below threshold (100 remaining)fetchAndLogRateLimitat start and end for observabilitydelayfromexpired_entity_cleanup_helpers.cjs)MAX_LIST_PAGES(50 pages / 5000 caches) to prevent runaway API usageOn-demand trigger
The
clean_cache_memoriesoperation is available in theworkflow_dispatchinputs, allowing manual triggering of the cache cleanup job without running other scheduled maintenance tasks.Changes
New files
actions/setup/js/cleanup_cache_memory.cjs- Cache cleanup scriptactions/setup/js/cleanup_cache_memory.test.cjs- 21 unit tests covering:actions/setup/js/rate_limit_helpers.cjs- Shared rate limit checking utilities (getRateLimitRemaining,checkRateLimit,MIN_RATE_LIMIT_REMAINING) withfetchAndLogRateLimitintegrationactions/setup/js/rate_limit_helpers.test.cjs- 6 unit tests for rate limit helpersModified files
pkg/workflow/maintenance_workflow.go- Addscleanup-cache-memoryjob generation andclean_cache_memoriesoperationpkg/workflow/maintenance_workflow_test.go- Includes new job and operation in condition checks.github/workflows/agentics-maintenance.yml- Regenerated with new job and operation