1- import { existsSync , promises as fs } from "node:fs" ;
1+ import { existsSync , readFileSync , promises as fs } from "node:fs" ;
22import { join } from "node:path" ;
33import { getCodexMultiAuthDir } from "../runtime-paths.js" ;
44
@@ -48,6 +48,7 @@ export interface RuntimeObservabilitySnapshot {
4848const SNAPSHOT_FILE_NAME = "runtime-observability.json" ;
4949const PERSIST_RUNTIME_SNAPSHOT = process . env . VITEST !== "true" ;
5050const RUNTIME_OBSERVABILITY_SNAPSHOT_VERSION = 1 ;
51+ const RETRYABLE_SNAPSHOT_ERRORS = new Set ( [ "EBUSY" , "EPERM" ] ) ;
5152
5253let snapshotState : RuntimeObservabilitySnapshot | null = null ;
5354let pendingWrite : Promise < void > | null = null ;
@@ -99,46 +100,102 @@ function createDefaultSnapshot(): RuntimeObservabilitySnapshot {
99100 } ;
100101}
101102
103+ function normalizePersistedSnapshot (
104+ parsed : Partial < RuntimeObservabilitySnapshot > | null ,
105+ ) : RuntimeObservabilitySnapshot | null {
106+ if ( ! parsed || typeof parsed !== "object" ) {
107+ return null ;
108+ }
109+ if (
110+ typeof parsed . version === "number" &&
111+ parsed . version !== RUNTIME_OBSERVABILITY_SNAPSHOT_VERSION
112+ ) {
113+ return null ;
114+ }
115+ const base = createDefaultSnapshot ( ) ;
116+ return {
117+ ...base ,
118+ ...parsed ,
119+ version : RUNTIME_OBSERVABILITY_SNAPSHOT_VERSION ,
120+ runtimeMetrics : {
121+ ...base . runtimeMetrics ,
122+ ...( parsed . runtimeMetrics ?? { } ) ,
123+ } ,
124+ } ;
125+ }
126+
127+ function loadPersistedRuntimeObservabilitySnapshotSync ( ) : RuntimeObservabilitySnapshot | null {
128+ const path = getSnapshotPath ( ) ;
129+ if ( ! existsSync ( path ) ) {
130+ return null ;
131+ }
132+ try {
133+ const raw = readFileSync ( path , "utf-8" ) ;
134+ const parsed = JSON . parse ( raw ) as Partial < RuntimeObservabilitySnapshot > | null ;
135+ return normalizePersistedSnapshot ( parsed ) ;
136+ } catch {
137+ return null ;
138+ }
139+ }
140+
141+ function ensureSnapshotState ( ) : RuntimeObservabilitySnapshot {
142+ if ( ! snapshotState ) {
143+ snapshotState =
144+ ( PERSIST_RUNTIME_SNAPSHOT
145+ ? loadPersistedRuntimeObservabilitySnapshotSync ( )
146+ : null ) ?? createDefaultSnapshot ( ) ;
147+ }
148+ return snapshotState ;
149+ }
150+
102151async function writeSnapshot ( snapshot : RuntimeObservabilitySnapshot ) : Promise < void > {
103152 const dir = getCodexMultiAuthDir ( ) ;
104153 const path = getSnapshotPath ( ) ;
105154 await fs . mkdir ( dir , { recursive : true } ) ;
106- const tempPath = `${ path } .${ process . pid } .${ Date . now ( ) } .tmp` ;
107- let moved = false ;
108- try {
109- await fs . writeFile ( tempPath , JSON . stringify ( snapshot , null , 2 ) , "utf-8" ) ;
110- await fs . rename ( tempPath , path ) ;
111- moved = true ;
112- } finally {
113- if ( ! moved ) {
114- try {
115- await fs . unlink ( tempPath ) ;
116- } catch {
117- // Best-effort cleanup for interrupted writes.
155+ let lastError : unknown = null ;
156+ for ( let attempt = 0 ; attempt < 3 ; attempt += 1 ) {
157+ const tempPath = `${ path } .${ process . pid } .${ Date . now ( ) } .${ attempt } .tmp` ;
158+ let moved = false ;
159+ try {
160+ await fs . writeFile ( tempPath , JSON . stringify ( snapshot , null , 2 ) , "utf-8" ) ;
161+ await fs . rename ( tempPath , path ) ;
162+ moved = true ;
163+ return ;
164+ } catch ( error ) {
165+ lastError = error ;
166+ const code = ( error as NodeJS . ErrnoException | undefined ) ?. code ?? "" ;
167+ if ( ! RETRYABLE_SNAPSHOT_ERRORS . has ( code ) || attempt >= 2 ) {
168+ throw error ;
169+ }
170+ } finally {
171+ if ( ! moved ) {
172+ try {
173+ await fs . unlink ( tempPath ) ;
174+ } catch {
175+ // Best-effort cleanup for interrupted writes.
176+ }
118177 }
119178 }
120179 }
180+ if ( lastError ) {
181+ throw lastError ;
182+ }
121183}
122184
123185export function getRuntimeObservabilitySnapshot ( ) : RuntimeObservabilitySnapshot {
124- if ( ! snapshotState ) {
125- snapshotState = createDefaultSnapshot ( ) ;
126- }
127- return structuredClone ( snapshotState ) ;
186+ return structuredClone ( ensureSnapshotState ( ) ) ;
128187}
129188
130189export function mutateRuntimeObservabilitySnapshot (
131190 mutator : ( snapshot : RuntimeObservabilitySnapshot ) => void ,
132191) : void {
133- if ( ! snapshotState ) {
134- snapshotState = createDefaultSnapshot ( ) ;
135- }
136- mutator ( snapshotState ) ;
137- snapshotState . updatedAt = Date . now ( ) ;
192+ const snapshot = ensureSnapshotState ( ) ;
193+ mutator ( snapshot ) ;
194+ snapshot . updatedAt = Date . now ( ) ;
138195 if ( ! PERSIST_RUNTIME_SNAPSHOT ) {
139196 return ;
140197 }
141- const nextSnapshot = structuredClone ( snapshotState ) ;
198+ const nextSnapshot = structuredClone ( snapshot ) ;
142199 pendingWrite = ( pendingWrite ?? Promise . resolve ( ) )
143200 . catch ( ( ) => undefined )
144201 . then ( ( ) => writeSnapshot ( nextSnapshot ) ) ;
@@ -152,25 +209,7 @@ export async function loadPersistedRuntimeObservabilitySnapshot(): Promise<Runti
152209 try {
153210 const raw = await fs . readFile ( path , "utf-8" ) ;
154211 const parsed = JSON . parse ( raw ) as Partial < RuntimeObservabilitySnapshot > | null ;
155- if ( ! parsed || typeof parsed !== "object" ) {
156- return null ;
157- }
158- if (
159- typeof parsed . version === "number" &&
160- parsed . version !== RUNTIME_OBSERVABILITY_SNAPSHOT_VERSION
161- ) {
162- return null ;
163- }
164- const base = createDefaultSnapshot ( ) ;
165- return {
166- ...base ,
167- ...parsed ,
168- version : RUNTIME_OBSERVABILITY_SNAPSHOT_VERSION ,
169- runtimeMetrics : {
170- ...base . runtimeMetrics ,
171- ...( parsed . runtimeMetrics ?? { } ) ,
172- } ,
173- } ;
212+ return normalizePersistedSnapshot ( parsed ) ;
174213 } catch {
175214 return null ;
176215 }
0 commit comments