@@ -57,8 +57,10 @@ const ROTATING_BACKUP_STALE_ARTIFACT_MAX_AGE_MS = 60_000;
5757export const NAMED_BACKUP_LIST_CONCURRENCY = 8 ;
5858export const ACCOUNT_SNAPSHOT_RETENTION_PER_REASON = 3 ;
5959const RESET_MARKER_SUFFIX = ".reset-intent" ;
60+ const AUTO_SNAPSHOT_MARKER_SUFFIX = ".snapshot-auto" ;
6061let storageBackupEnabled = true ;
6162let lastAccountsSaveTimestamp = 0 ;
63+ const retentionEnforcementInFlight = new Set < string > ( ) ;
6264
6365export interface FlaggedAccountMetadataV1 extends AccountMetadataV3 {
6466 flaggedAt : number ;
@@ -1880,8 +1882,9 @@ export async function getRestoreAssessment(): Promise<RestoreAssessment> {
18801882 } ;
18811883}
18821884
1883- async function scanNamedBackups ( ) : Promise < NamedBackupScanResult > {
1884- const backupRoot = getNamedBackupRoot ( getStoragePath ( ) ) ;
1885+ async function scanNamedBackups (
1886+ backupRoot = getNamedBackupRoot ( getStoragePath ( ) ) ,
1887+ ) : Promise < NamedBackupScanResult > {
18851888 try {
18861889 const entries = await retryTransientFilesystemOperation ( ( ) =>
18871890 fs . readdir ( backupRoot , { withFileTypes : true } ) ,
@@ -1949,8 +1952,9 @@ async function scanNamedBackups(): Promise<NamedBackupScanResult> {
19491952 }
19501953}
19511954
1952- async function listNamedBackupsWithoutLoading ( ) : Promise < NamedBackupMetadataListingResult > {
1953- const backupRoot = getNamedBackupRoot ( getStoragePath ( ) ) ;
1955+ async function listNamedBackupsWithoutLoading (
1956+ backupRoot = getNamedBackupRoot ( getStoragePath ( ) ) ,
1957+ ) : Promise < NamedBackupMetadataListingResult > {
19541958 try {
19551959 const entries = await retryTransientFilesystemOperation ( ( ) =>
19561960 fs . readdir ( backupRoot , { withFileTypes : true } ) ,
@@ -2088,7 +2092,12 @@ export function detectOpencodeAccountPoolPath(): string | null {
20882092 const explicit = process . env . CODEX_OPENCODE_POOL_PATH ;
20892093 if ( explicit ?. trim ( ) ) {
20902094 const explicitPath = resolvePath ( explicit . trim ( ) ) ;
2091- return existsSync ( explicitPath ) ? explicitPath : null ;
2095+ if ( ! existsSync ( explicitPath ) ) {
2096+ throw new Error (
2097+ `CODEX_OPENCODE_POOL_PATH points to a file that does not exist: ${ basename ( explicitPath ) } ` ,
2098+ ) ;
2099+ }
2100+ return explicitPath ;
20922101 }
20932102
20942103 const appDataBases = [ process . env . LOCALAPPDATA , process . env . APPDATA ] . filter (
@@ -2322,6 +2331,43 @@ export function isAccountSnapshotName(name: string): boolean {
23222331 ) ;
23232332}
23242333
2334+ function getAutoSnapshotMarkerPath ( backupPath : string ) : string {
2335+ return `${ backupPath } ${ AUTO_SNAPSHOT_MARKER_SUFFIX } ` ;
2336+ }
2337+
2338+ function normalizeStorageComparisonKey ( pathValue : string ) : string {
2339+ const resolvedPath = resolvePath ( pathValue ) ;
2340+ return process . platform === "win32"
2341+ ? resolvedPath . toLowerCase ( )
2342+ : resolvedPath ;
2343+ }
2344+
2345+ async function writeAutoSnapshotMarker (
2346+ backupPath : string ,
2347+ reason : AccountSnapshotReason ,
2348+ ) : Promise < void > {
2349+ const markerPath = getAutoSnapshotMarkerPath ( backupPath ) ;
2350+ await retryTransientFilesystemOperation ( ( ) =>
2351+ fs . writeFile ( markerPath , reason , { encoding : "utf-8" , mode : 0o600 } ) ,
2352+ ) ;
2353+ }
2354+
2355+ async function deleteAutoSnapshotMarker ( backupPath : string ) : Promise < void > {
2356+ const markerPath = getAutoSnapshotMarkerPath ( backupPath ) ;
2357+ try {
2358+ await unlinkWithRetry ( markerPath ) ;
2359+ } catch ( error ) {
2360+ const code = ( error as NodeJS . ErrnoException ) . code ;
2361+ if ( code !== "ENOENT" ) {
2362+ throw error ;
2363+ }
2364+ }
2365+ }
2366+
2367+ function isMarkedAutoSnapshot ( backupPath : string ) : boolean {
2368+ return existsSync ( getAutoSnapshotMarkerPath ( backupPath ) ) ;
2369+ }
2370+
23252371function getAccountSnapshotReason ( name : string ) : string | null {
23262372 const match = name . match (
23272373 / ^ a c c o u n t s - (?< reason > [ a - z 0 - 9 - ] + ) - s n a p s h o t - \d { 4 } - \d { 2 } - \d { 2 } _ \d { 2 } - \d { 2 } - \d { 2 } _ \d { 3 } $ / i,
@@ -2389,6 +2435,7 @@ export interface AutoSnapshotPruneOptions {
23892435 backups ?: NamedBackupMetadata [ ] ;
23902436 preserveNames ?: Iterable < string > ;
23912437 keepLatestPerReason ?: number ;
2438+ storagePath ?: string ;
23922439}
23932440
23942441export interface AutoSnapshotPruneResult {
@@ -2399,7 +2446,13 @@ export interface AutoSnapshotPruneResult {
23992446export async function pruneAutoGeneratedSnapshots (
24002447 options : AutoSnapshotPruneOptions = { } ,
24012448) : Promise < AutoSnapshotPruneResult > {
2402- const backups = options . backups ?? ( await listNamedBackups ( ) ) ;
2449+ const backups =
2450+ options . backups ??
2451+ (
2452+ await scanNamedBackups (
2453+ getNamedBackupRoot ( options . storagePath ?? getStoragePath ( ) ) ,
2454+ )
2455+ ) . backups . map ( ( entry ) => entry . backup ) ;
24032456 const keepLatestPerReason = Math . max (
24042457 1 ,
24052458 options . keepLatestPerReason ?? 1 ,
@@ -2410,7 +2463,12 @@ export async function pruneAutoGeneratedSnapshots(
24102463 }
24112464
24122465 const autoSnapshots = backups
2413- . map ( ( backup ) => parseAutoSnapshot ( backup ) )
2466+ . map ( ( backup ) => {
2467+ if ( ! isMarkedAutoSnapshot ( backup . path ) ) {
2468+ return null ;
2469+ }
2470+ return parseAutoSnapshot ( backup ) ;
2471+ } )
24142472 . filter ( ( snapshot ) : snapshot is AutoSnapshotDetails => snapshot !== null ) ;
24152473 if ( autoSnapshots . length === 0 ) {
24162474 return { pruned : [ ] , kept : [ ] } ;
@@ -2439,6 +2497,7 @@ export async function pruneAutoGeneratedSnapshots(
24392497 }
24402498 try {
24412499 await unlinkWithRetry ( snapshot . backup . path ) ;
2500+ await deleteAutoSnapshotMarker ( snapshot . backup . path ) ;
24422501 pruned . push ( snapshot . backup ) ;
24432502 } catch ( error ) {
24442503 keptNames . add ( snapshot . name ) ;
@@ -2484,23 +2543,25 @@ export function formatRedactedFilesystemError(error: unknown): string {
24842543}
24852544
24862545function formatSnapshotErrorForLog ( error : unknown ) : string {
2487- const code =
2488- typeof ( error as NodeJS . ErrnoException | undefined ) ?. code === "string"
2489- ? ( error as NodeJS . ErrnoException ) . code
2490- : undefined ;
2491- const rawMessage =
2492- error instanceof Error ? error . message : String ( error ?? "unknown error" ) ;
2493- const redactedMessage = redactFilesystemDetails ( rawMessage ) ;
2494- if ( code && ! redactedMessage . includes ( code ) ) {
2495- return `${ code } : ${ redactedMessage } ` ;
2496- }
2497- return redactedMessage ;
2546+ return formatRedactedFilesystemError ( error ) ;
24982547}
24992548
2500- async function enforceSnapshotRetention ( ) : Promise < void > {
2501- await pruneAutoGeneratedSnapshots ( {
2502- keepLatestPerReason : ACCOUNT_SNAPSHOT_RETENTION_PER_REASON ,
2503- } ) ;
2549+ async function enforceSnapshotRetention ( storagePath ?: string ) : Promise < void > {
2550+ const resolvedStoragePath = normalizeStorageComparisonKey (
2551+ storagePath ?? getStoragePath ( ) ,
2552+ ) ;
2553+ if ( retentionEnforcementInFlight . has ( resolvedStoragePath ) ) {
2554+ return ;
2555+ }
2556+ retentionEnforcementInFlight . add ( resolvedStoragePath ) ;
2557+ try {
2558+ await pruneAutoGeneratedSnapshots ( {
2559+ keepLatestPerReason : ACCOUNT_SNAPSHOT_RETENTION_PER_REASON ,
2560+ storagePath : resolvedStoragePath ,
2561+ } ) ;
2562+ } finally {
2563+ retentionEnforcementInFlight . delete ( resolvedStoragePath ) ;
2564+ }
25042565}
25052566
25062567export async function snapshotAccountStorage (
@@ -2545,7 +2606,17 @@ export async function snapshotAccountStorage(
25452606 }
25462607
25472608 try {
2548- await enforceSnapshotRetention ( ) ;
2609+ await writeAutoSnapshotMarker ( snapshot . path , reason ) ;
2610+ } catch ( error ) {
2611+ log . warn ( "Failed to mark account storage snapshot for retention" , {
2612+ reason,
2613+ backupName,
2614+ error : formatSnapshotErrorForLog ( error ) ,
2615+ } ) ;
2616+ }
2617+
2618+ try {
2619+ await enforceSnapshotRetention ( resolvedStoragePath ) ;
25492620 } catch ( error ) {
25502621 log . warn ( "Failed to enforce account snapshot retention" , {
25512622 reason,
@@ -3067,9 +3138,11 @@ function equalsNamedBackupEntry(left: string, right: string): boolean {
30673138}
30683139
30693140function equalsResolvedStoragePath ( left : string , right : string ) : boolean {
3141+ const normalizedLeft = resolvePath ( left ) ;
3142+ const normalizedRight = resolvePath ( right ) ;
30703143 return process . platform === "win32"
3071- ? left . toLowerCase ( ) === right . toLowerCase ( )
3072- : left === right ;
3144+ ? normalizedLeft . toLowerCase ( ) === normalizedRight . toLowerCase ( )
3145+ : normalizedLeft === normalizedRight ;
30733146}
30743147
30753148function stripNamedBackupJsonExtension ( name : string ) : string {
0 commit comments