From 13397a7b2f75865fc99ff7ca378e1cd76e0cef70 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:18:21 -0800 Subject: [PATCH] Stop validation work once preparation cannot be reused --- .../codex-hook-launch-preparation.ts | 23 ++++++++++++------- .../codex-launch-preparation-inputs.ts | 21 ++++++++++++++++- .../filesystem/file-content-observation.ts | 21 +++++++++-------- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/main/codex/hooks/installation/codex-hook-launch-preparation.ts b/src/main/codex/hooks/installation/codex-hook-launch-preparation.ts index e4ff6fdf550..ea6ebbac799 100644 --- a/src/main/codex/hooks/installation/codex-hook-launch-preparation.ts +++ b/src/main/codex/hooks/installation/codex-hook-launch-preparation.ts @@ -169,14 +169,21 @@ export class CodexHookLaunchPreparation { return run() } let complete = false - const result = await withFileContentObservation(before.files, before.paths, async () => { - const status = await run(() => { - complete = true - }) - // This read also validates the last exact publications against the observation scope. - const after = readCodexLaunchPreparationInputs(homePath) - return { status, stable: after?.digest === before.digest } - }) + const result = await withFileContentObservation( + before.files, + before.paths, + async (isStillValid) => { + const status = await run(() => { + complete = true + }) + // This read also validates the last exact publications against the observation scope. + const stable = + complete && + isStillValid() && + readCodexLaunchPreparationInputs(homePath, isStillValid)?.digest === before.digest + return { status, stable } + } + ) if (complete && result.matches && result.value.stable && digest !== null) { this.completedLaunchPrep.set(key, { digest, status: { ...result.value.status } }) if (this.completedLaunchPrep.size > 32) { diff --git a/src/main/codex/hooks/installation/codex-launch-preparation-inputs.ts b/src/main/codex/hooks/installation/codex-launch-preparation-inputs.ts index 34a4b5e7a0f..5425cfb5697 100644 --- a/src/main/codex/hooks/installation/codex-launch-preparation-inputs.ts +++ b/src/main/codex/hooks/installation/codex-launch-preparation-inputs.ts @@ -28,7 +28,8 @@ export type CodexLaunchPreparationInputs = { /** Unknown inputs only prevent reuse; preparation retains its existing error/retry behavior. */ export function readCodexLaunchPreparationInputs( - runtimeHomePath: string + runtimeHomePath: string, + isStillValid: () => boolean = () => true ): CodexLaunchPreparationInputs | null { try { const systemHome = getSystemCodexHomePath() @@ -41,13 +42,22 @@ export function readCodexLaunchPreparationInputs( observeResolvedFilePath(kind, input, output) } for (const home of homes) { + if (!isStillValid()) { + return null + } const resolvedHome = resolveHome(home) facts.push([home, resolvedHome]) recordPath('marketplace-home', home, resolvedHome) recordPath('daemon-home', home, resolvedHome) for (const leaf of ['config.toml', 'hooks.json']) { + if (!isStillValid()) { + return null + } const filePath = join(home, leaf) files.push(snapshotFile(filePath, facts)) + if (!isStillValid()) { + return null + } const writePath = leaf === 'hooks.json' ? resolveHooksJsonWritePath(filePath) @@ -73,8 +83,14 @@ export function readCodexLaunchPreparationInputs( getLegacyCodexProfileTomlPath(), getManagedScriptPath() ]) { + if (!isStillValid()) { + return null + } files.push(snapshotFile(filePath, facts)) } + if (!isStillValid()) { + return null + } const scriptPath = getManagedScriptPath() // A mode repair is not a fixed point, even if another writer undoes chmod before the final read. const scriptMode = statSync(scriptPath).mode & 0o777 @@ -87,6 +103,9 @@ export function readCodexLaunchPreparationInputs( recordPath('toml-write', legacyPath, legacyWritePath) facts.push([`${legacyWritePath}.bak`, entryType(`${legacyWritePath}.bak`)]) facts.push([...paths], files) + if (!isStillValid()) { + return null + } return { digest: digestFileContent(JSON.stringify(facts)), files, paths } } catch { return null diff --git a/src/main/filesystem/file-content-observation.ts b/src/main/filesystem/file-content-observation.ts index 559c11a71c7..9458e67bcfc 100644 --- a/src/main/filesystem/file-content-observation.ts +++ b/src/main/filesystem/file-content-observation.ts @@ -30,7 +30,7 @@ export function digestFileContent(content: string | Uint8Array): string { export async function withFileContentObservation( files: readonly ObservedFileContent[], paths: ReadonlyMap, - run: () => Promise + run: (isStillValid: () => boolean) => Promise ): Promise<{ value: T; matches: boolean }> { const expected = new Map() for (const file of files) { @@ -39,7 +39,7 @@ export async function withFileContentObservation( const key = fileKey(alias) if (expected.has(key)) { // Ambiguous aliases cannot prove which file an operation processed. - return { value: await run(), matches: false } + return { value: await run(() => false), matches: false } } expected.set(key, entry) } @@ -52,7 +52,7 @@ export async function withFileContentObservation( } return observations.run(observation, async () => { try { - const value = await run() + const value = await run(() => observation.open && observation.matches) return { value, matches: observation.matches } } finally { observation.open = false @@ -64,7 +64,7 @@ export async function withFileContentObservation( export function observeFileDigest(filePath: string, digest: string | null): void { const observation = observations.getStore() - if (!observation?.open) { + if (!observation?.open || !observation.matches) { return } const expected = observation.files.get(fileKey(filePath)) @@ -75,7 +75,7 @@ export function observeFileDigest(filePath: string, digest: string | null): void export function observeFileContent(filePath: string, content: string | Uint8Array): void { const observation = observations.getStore() - if (!observation?.open || !observation.files.has(fileKey(filePath))) { + if (!observation?.open || !observation.matches || !observation.files.has(fileKey(filePath))) { return } observeFileDigest(filePath, digestFileContent(content)) @@ -83,7 +83,7 @@ export function observeFileContent(filePath: string, content: string | Uint8Arra export function observeFileReadError(filePath: string, error: unknown): void { const observation = observations.getStore() - if (!observation?.open || !observation.files.has(fileKey(filePath))) { + if (!observation?.open || !observation.matches || !observation.files.has(fileKey(filePath))) { return } if (isDefinitiveAbsence(error)) { @@ -95,7 +95,7 @@ export function observeFileReadError(filePath: string, error: unknown): void { export function invalidateFileObservation(filePath: string): void { const observation = observations.getStore() - if (observation?.open && observation.files.has(fileKey(filePath))) { + if (observation?.open && observation.matches && observation.files.has(fileKey(filePath))) { observation.matches = false } } @@ -105,7 +105,7 @@ export function observeFilePublication( content: string | Uint8Array | null ): void { const observation = observations.getStore() - if (!observation?.open) { + if (!observation?.open || !observation.matches) { return } const expected = observation.files.get(fileKey(filePath)) @@ -116,7 +116,7 @@ export function observeFilePublication( export function observeResolvedFilePath(kind: string, input: string, resolved: string): string { const observation = observations.getStore() - if (!observation?.open) { + if (!observation?.open || !observation.matches) { return resolved } const expected = observation.paths.get(`${kind}\0${input}`) @@ -130,6 +130,7 @@ export function observeResolvedFilePathError(kind: string, input: string, error: const observation = observations.getStore() if ( observation?.open && + observation.matches && observation.paths.has(`${kind}\0${input}`) && !isDefinitiveAbsence(error) ) { @@ -140,7 +141,7 @@ export function observeResolvedFilePathError(kind: string, input: string, error: export function observedExistsSync(filePath: string): boolean { const present = existsSync(filePath) const observation = observations.getStore() - if (observation?.open) { + if (observation?.open && observation.matches) { const expected = observation.files.get(fileKey(filePath)) if (expected && present !== (expected.digest !== null)) { observation.matches = false