When adding concurrency behavior (locks, in-flight guards, timeouts, retries, sampling), ensure three properties:
1) Atomic “check-and-act” before any await (avoid TOCTOU)
finally).2) Revalidate decisions against a fresh snapshot (avoid stale-snapshot races)
3) Cleanup must drain/await in-flight work even on failure (avoid teardown ordering bugs)
finally (or explicitly cancel them) so teardown (mode restore, session finalize) can’t occur while writes are still pending.Practical patterns
A) Atomic in-flight reservation
const inFlight = new Set<string>();
async function createSession(id: string) {
// synchronous check-and-add before any await
if (inFlight.has(id)) throw new Error('409 conflict');
inFlight.add(id);
try {
// do awaits/spawn here
await doWork();
} finally {
inFlight.delete(id);
}
}
B) Generation guard for out-of-order async completion
let gen = 0;
const lastApplied = { current: -1 };
async function refresh() {
const myGen = ++gen;
const value = await compute();
if (myGen !== gen) return; // stale completion
if (lastApplied.current !== myGen) {
lastApplied.current = myGen;
setState(value);
}
}
C) Double-sample + drift flag
const headBefore = await getPrHead();
const comments = await fetchAllComments();
const headAfter = await getPrHead();
const drift = headBefore !== headAfter;
// If drift, mark threads as staleWorktree or fail-closed.
D) Drain async writes in finally
let outputWrites = Promise.resolve();
try {
await pumpInputToPty();
await outputWrites;
return;
} finally {
// ensure pending writes finish or are cancelled before teardown
await outputWrites.catch(() => {});
}
If any change touches session creation, process spawning, locks/reservations, per-attempt retries, terminal/IO pipelines, or sampling across async boundaries, apply this checklist and add regression tests for the “race-changed-decision” branches—not just the happy path.