Stop validation work once preparation cannot be reused

This commit is contained in:
Brennan Benson
2026-11-13 17:18:21 -08:00
parent eb76aca797
commit 13397a7b2f
3 changed files with 46 additions and 19 deletions
@@ -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) {
@@ -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
+11 -10
View File
@@ -30,7 +30,7 @@ export function digestFileContent(content: string | Uint8Array): string {
export async function withFileContentObservation<T>(
files: readonly ObservedFileContent[],
paths: ReadonlyMap<string, string>,
run: () => Promise<T>
run: (isStillValid: () => boolean) => Promise<T>
): Promise<{ value: T; matches: boolean }> {
const expected = new Map<string, { digest: string | null }>()
for (const file of files) {
@@ -39,7 +39,7 @@ export async function withFileContentObservation<T>(
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<T>(
}
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<T>(
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