@@ -20,6 +20,9 @@ const { getErrorMessage } = require("./error_helpers.cjs");
2020/** Path where the restored (and updated) usage cache lives on the runner. */
2121const CACHE_FILE_PATH = "/tmp/gh-aw/agentic-workflow-usage-cache.jsonl" ;
2222
23+ /** Entries older than this threshold (in ms) are pruned when rewriting the cache. */
24+ const CACHE_RETENTION_MS = 48 * 60 * 60 * 1000 ;
25+
2326/**
2427 * Directory prepared by the "Collect usage artifact files" step in the conclusion job.
2528 * Contains agent_usage.jsonl and agent/token_usage.jsonl which mirror the contents of
@@ -47,12 +50,18 @@ function logCache(message, details) {
4750}
4851
4952/**
50- * Appends a `{run_id, aic}` JSONL entry to the cache file, preserving any existing entries
51- * that were restored from the previous cache snapshot.
53+ * Appends a `{run_id, aic, timestamp}` JSONL entry to the cache file, preserving any existing
54+ * entries that were restored from the previous cache snapshot and are within the 48-hour
55+ * retention window. Entries older than {@link CACHE_RETENTION_MS} are pruned to keep the
56+ * cache file bounded.
5257 *
58+ * @param {string } [cacheFilePath] Override the cache file path (defaults to {@link CACHE_FILE_PATH}; useful in tests).
59+ * @param {string } [usageDir] Override the usage directory (defaults to {@link USAGE_DIR}; useful in tests).
5360 * @returns {Promise<void> }
5461 */
55- async function main ( ) {
62+ async function mainWithPaths ( cacheFilePath , usageDir ) {
63+ const cachePath = cacheFilePath || CACHE_FILE_PATH ;
64+ const usageDirPath = usageDir || USAGE_DIR ;
5665 try {
5766 const runId = Number ( process . env . GITHUB_RUN_ID || 0 ) ;
5867 if ( ! runId ) {
@@ -61,8 +70,8 @@ async function main() {
6170 }
6271
6372 // Compute AIC from the usage JSONL files prepared by buildUsageArtifactUploadSteps.
64- const usageFiles = findJSONLFiles ( USAGE_DIR ) ;
65- logCache ( "Scanning usage JSONL files" , { dir : USAGE_DIR , count : usageFiles . length , files : usageFiles } ) ;
73+ const usageFiles = findJSONLFiles ( usageDirPath ) ;
74+ logCache ( "Scanning usage JSONL files" , { dir : usageDirPath , count : usageFiles . length , files : usageFiles } ) ;
6675 const aic = sumAICFromUsageJSONLFiles ( usageFiles ) ;
6776 logCache ( "Computed AIC for current run" , { runId, aic } ) ;
6877
@@ -76,32 +85,67 @@ async function main() {
7685 }
7786
7887 // Read existing cache content (restored from the previous run's cache snapshot, if any).
79- let existingLines = "" ;
88+ // Entries with a `timestamp` older than CACHE_RETENTION_MS are pruned to keep the file
89+ // bounded. Entries without a `timestamp` (written by an older version of this script)
90+ // are preserved for backward compatibility.
91+ /** @type {string[] } */
92+ let keptLines = [ ] ;
8093 try {
81- if ( fs . existsSync ( CACHE_FILE_PATH ) ) {
82- existingLines = fs . readFileSync ( CACHE_FILE_PATH , "utf8" ) . trimEnd ( ) ;
83- const lineCount = existingLines ? existingLines . split ( "\n" ) . length : 0 ;
84- logCache ( "Loaded existing cache entries" , { path : CACHE_FILE_PATH , lineCount } ) ;
94+ if ( fs . existsSync ( cachePath ) ) {
95+ const raw = fs . readFileSync ( cachePath , "utf8" ) . trimEnd ( ) ;
96+ const now = Date . now ( ) ;
97+ const cutoff = now - CACHE_RETENTION_MS ;
98+ let total = 0 ;
99+ let pruned = 0 ;
100+ for ( const rawLine of raw . split ( "\n" ) ) {
101+ const line = rawLine . trim ( ) ;
102+ if ( ! line ) continue ;
103+ total ++ ;
104+ try {
105+ const entry = JSON . parse ( line ) ;
106+ if ( typeof entry ?. timestamp === "string" ) {
107+ const ts = Date . parse ( entry . timestamp ) ;
108+ if ( Number . isFinite ( ts ) && ts < cutoff ) {
109+ pruned ++ ;
110+ continue ;
111+ }
112+ }
113+ keptLines . push ( line ) ;
114+ } catch {
115+ // Preserve lines that cannot be parsed (defensive: avoids data loss).
116+ keptLines . push ( line ) ;
117+ }
118+ }
119+ logCache ( "Loaded existing cache entries" , { path : cachePath , total, kept : keptLines . length , pruned } ) ;
85120 } else {
86- logCache ( "No existing cache file found; starting fresh" , { path : CACHE_FILE_PATH } ) ;
121+ logCache ( "No existing cache file found; starting fresh" , { path : cachePath } ) ;
87122 }
88123 } catch ( readErr ) {
89124 core . warning ( `[daily-aic-cache] Could not read existing cache file: ${ getErrorMessage ( readErr ) } ` ) ;
90125 }
91126
92127 // Build the updated JSONL content.
93- const newEntry = JSON . stringify ( { run_id : runId , aic } ) ;
94- const updatedContent = existingLines ? `${ existingLines } \n${ newEntry } \n` : `${ newEntry } \n` ;
128+ const newEntry = JSON . stringify ( { run_id : runId , aic, timestamp : new Date ( ) . toISOString ( ) } ) ;
129+ const updatedContent = keptLines . length > 0 ? `${ keptLines . join ( "\n" ) } \n${ newEntry } \n` : `${ newEntry } \n` ;
95130
96131 // Ensure the directory exists and write the updated file.
97- const dir = path . dirname ( CACHE_FILE_PATH ) ;
132+ const dir = path . dirname ( cachePath ) ;
98133 fs . mkdirSync ( dir , { recursive : true } ) ;
99- fs . writeFileSync ( CACHE_FILE_PATH , updatedContent , "utf8" ) ;
100- logCache ( "Wrote cache entry" , { runId, aic, path : CACHE_FILE_PATH } ) ;
134+ fs . writeFileSync ( cachePath , updatedContent , "utf8" ) ;
135+ logCache ( "Wrote cache entry" , { runId, aic, path : cachePath } ) ;
101136 } catch ( error ) {
102137 // Non-fatal: a cache write failure should never block the conclusion job.
103138 core . warning ( `[daily-aic-cache] Failed to write usage cache: ${ getErrorMessage ( error ) } ` ) ;
104139 }
105140}
106141
107- module . exports = { main } ;
142+ /**
143+ * Entry point called from the GitHub Actions step.
144+ *
145+ * @returns {Promise<void> }
146+ */
147+ async function main ( ) {
148+ return mainWithPaths ( ) ;
149+ }
150+
151+ module . exports = { main, mainWithPaths } ;
0 commit comments