Divergence in the opposite direction from the usual truncation reports — perry writes data node discards.
// five fire-and-forget writes, then exit in the same tick
for (let i = 0; i < 5; i++) fs.promises.appendFile(p, `line ${i}\n`);
process.exit(0);
node : 0 records
perry: 5 records
Node's process.exit() terminates without draining pending libuv work, so promises that have not yet performed their write are simply abandoned. Perry completes them.
Why it matters
process.exit() is the documented way to stop now, and real code relies on that: an error path that exits deliberately should not have partially-written or duplicate state committed behind it. Writing more than node is not a benign difference — it can commit records a program deliberately abandoned, and it means the same code produces different on-disk state under the two runtimes.
It is also a plausible source of confusion in the other direction: anyone debugging a truncation bug will see perry writing more here and may draw the wrong conclusion about which engine buffers.
Scope to establish
Whether this is specific to fs.promises.appendFile or general to pending async work at exit. Worth checking the callback form, createWriteStream, pending timers, pending sockets, and child_process writes — the fix differs a lot depending on whether one subsystem drains on shutdown or the exit path drains everything.
Note the process.on('exit') handler semantics are already known-divergent (recorded during the pi work: not fired on explicit exit), so check whether these share a root before fixing either.
Verification bar
A gap fixture byte-compared to node --experimental-strip-types, demonstrated failing on a compiler built from unfixed origin/main, covering: fire-and-forget promise writes, the callback form, createWriteStream with unflushed data, a pending setTimeout, and an awaited write that has genuinely completed before the exit (which must still land in both engines — that is the control that keeps a fix from over-abandoning).
Found by the differential stress-test of claude-code under perry, while falsifying #9421's async-flush premise.
Divergence in the opposite direction from the usual truncation reports — perry writes data node discards.
Node's
process.exit()terminates without draining pending libuv work, so promises that have not yet performed their write are simply abandoned. Perry completes them.Why it matters
process.exit()is the documented way to stop now, and real code relies on that: an error path that exits deliberately should not have partially-written or duplicate state committed behind it. Writing more than node is not a benign difference — it can commit records a program deliberately abandoned, and it means the same code produces different on-disk state under the two runtimes.It is also a plausible source of confusion in the other direction: anyone debugging a truncation bug will see perry writing more here and may draw the wrong conclusion about which engine buffers.
Scope to establish
Whether this is specific to
fs.promises.appendFileor general to pending async work at exit. Worth checking the callback form,createWriteStream, pending timers, pending sockets, andchild_processwrites — the fix differs a lot depending on whether one subsystem drains on shutdown or the exit path drains everything.Note the
process.on('exit')handler semantics are already known-divergent (recorded during the pi work: not fired on explicit exit), so check whether these share a root before fixing either.Verification bar
A gap fixture byte-compared to
node --experimental-strip-types, demonstrated failing on a compiler built from unfixedorigin/main, covering: fire-and-forget promise writes, the callback form,createWriteStreamwith unflushed data, a pendingsetTimeout, and an awaited write that has genuinely completed before the exit (which must still land in both engines — that is the control that keeps a fix from over-abandoning).Found by the differential stress-test of claude-code under perry, while falsifying #9421's async-flush premise.