fix(#3780): serialize WINDOWS.md ledger mutations on a cross-process lock (#4681)

* test(#3780): regression tests for parallel ledger-writer loss

* fix(#3780): serialize WINDOWS.md mutations on a cross-process ledger lock

* fix(#3780): keep the ledger-unavailable degrade contract intact under the lock wrapper

* chore(#3780): backfill changeset PR number (4681)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-13 05:13:42 -04:00
committed by GitHub
parent ccb39aec15
commit 0763326ced
6 changed files with 511 additions and 58 deletions

View File

@@ -406,6 +406,36 @@ function loadWindowsOrDegrade(
return { ok: true, windows, ledger };
}
/**
* #3780: lazy (module-load cost, see file header) require of the ledger's
* serialization seam — the same `.planning/.WINDOWS.lock` the windows cmd*
* writers hold, so this router's own read-compute-write cycles on
* WINDOWS.md cannot lose updates against them. Required from the REAL
* compiled module, never through the injectable `windowsOverride` seam:
* the lock is infrastructure, not a parser stand-in.
*/
function withLedgerLock<T>(cwd: string, fn: () => T): T {
let lockMod: {
withLedgerLock: (cwd: string, fn: () => T) => T;
};
try {
/* eslint-disable @typescript-eslint/no-require-imports */
lockMod = require('./broken-windows.cjs') as {
withLedgerLock: (cwd: string, fn: () => T) => T;
};
/* eslint-enable @typescript-eslint/no-require-imports */
} catch {
// Ledger module unavailable (the #1953 degrade world — see the row-86
// notesLedgerUnavailableWithoutBrokenWindows contract): no lock-holding
// writer can exist either, because every WINDOWS.md writer requires this
// same module. Run the body unlocked and let loadWindowsOrDegrade
// produce the canonical degrade note — wrapping that world in lock
// ceremony would only rewrite the note the degrade contract pins.
return fn();
}
return lockMod.withLedgerLock(cwd, fn);
}
/**
* Strict-mode window append (step 9 of `evaluate`). Degrades to
* `{ recorded: false, note }` per `loadWindowsOrDegrade` — never an error,
@@ -418,6 +448,28 @@ function recordStrictWindow(
padded: string,
target: Candidate,
windowsOverride: WindowsModule | undefined,
): { recorded: boolean; note?: string } {
// #3780: hold the same cross-process ledger lock the windows cmd* writers
// hold — this site's read-compute-write on WINDOWS.md is otherwise the
// same lost-update race: a concurrent `gsd_run windows append` (or another
// evaluator) could silently overwrite this entry, or be overwritten by
// it. The lock comes from the real ledger module, not the injectable
// `windowsOverride` seam — it is infrastructure, not a parser stand-in.
// The wrapper keeps the #1953-defect-2 degrade contract: the lock's typed
// refusal degrades to `{ recorded: false, note }` like every other
// failure, it never throws out of this function.
try {
return withLedgerLock(cwd, () => recordStrictWindowLocked(cwd, padded, target, windowsOverride));
} catch (e) {
return { recorded: false, note: `failed to record broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
}
}
function recordStrictWindowLocked(
cwd: string,
padded: string,
target: Candidate,
windowsOverride: WindowsModule | undefined,
): { recorded: boolean; note?: string } {
const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
if (!loaded.ok) return { recorded: false, note: loaded.note };
@@ -460,6 +512,26 @@ function resolveLedgerWindow(
kind: 'accept' | 'decline',
reasonText: string,
windowsOverride: WindowsModule | undefined,
): { resolved: boolean; note?: string } {
// #3780: same serialization as recordStrictWindow — a concurrent ledger
// writer holding a stale snapshot would silently revert this resolve (or
// lose its own write to this one). Same degrade contract: the lock's
// typed refusal degrades, never throws.
try {
return withLedgerLock(cwd, () => resolveLedgerWindowLocked(cwd, padded, file, line, kind, reasonText, windowsOverride));
} catch (e) {
return { resolved: false, note: `failed to resolve broken-windows entry: ${e instanceof Error ? e.message : String(e)}` };
}
}
function resolveLedgerWindowLocked(
cwd: string,
padded: string,
file: string,
line: number,
kind: 'accept' | 'decline',
reasonText: string,
windowsOverride: WindowsModule | undefined,
): { resolved: boolean; note?: string } {
const loaded = loadWindowsOrDegrade(cwd, windowsOverride);
if (!loaded.ok) return { resolved: false, note: loaded.note };