You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In appendLines, the partial-write rollback is gated behind closing the append handle. If handle.close() throws (common on real disk failures — ENOSPC/EIO), rollbackAppend never runs, partial bytes stay in the log, the retry re-appends the same sequence numbers, and the session becomes permanently unloadable.
/** * Append and fsync event lines. On a partial write or sync failure, restore the * previous size before rethrowing because the unchanged cursor will retry the * batch; leaving partial bytes would create duplicate sequence numbers. */privateasyncappendLines(meta: SessionHeader,events: readonlySessionEvent[]): Promise<void>{constcontent=awaitthis.encodeEventBatch(events)constpath=logPath(this.root,meta.cwd,meta.id,this.compression)consthandle=awaitopen(path,'a')letclosed=falseconstcloseAppendHandle=async(): Promise<void>=>{if(closed)returnclosed=trueawaithandle.close()}try{const{size: before}=awaithandle.stat()try{awaithandle.writeFile(content)awaithandle.sync()}catch(error){try{awaitcloseAppendHandle()// ← closes FIRSTawaitthis.rollbackAppend(path,before)// ← rollback never runs if close throws}catch(rollbackError){thrownewAggregateError([error,rollbackError],`failed to roll back append to "${path}"`)}throwerror}}finally{awaitcloseAppendHandle()}}
The comment itself states the failure mode: "leaving partial bytes would create duplicate sequence numbers." The rollback (truncate(before) + fsync, rollbackAppend at 681-689) is scheduled aftercloseAppendHandle(). When the write/sync fails due to a disk fault, close() frequently fails too (same fault). Then:
Partial bytes remain at the tail.
SessionWriteBehind.startWrite (write-behind.ts:145-152) re-queues the same batch and retries.
The second append writes events with the same sequence numbers again.
SessionLogScanner.consumeEventLine (format.ts:364-371) rejects non-contiguous seq in the committed region → wrapped as SessionPersistenceCorruptionError (coordinator.ts:926-929) → the whole session cannot be loaded (verified: a duplicate-seq log is rejected by the scanner).
Rollback and close are two independent cleanup actions; one failing must not block the other.
Trigger
Append-time disk error + handle-close failure (low probability, but the consequence is whole-session corruption rather than a lost batch).
Suggested fix
Run rollbackAppendbefore closing the handle; merge both errors via AggregateError — the rollback must never be skipped because the close failed. (Note the outer finally still closes the handle, so closing early is redundant anyway.)
Verified at master 47f9438; duplicate-seq rejection by the scanner was reproduced locally with Node v22.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
In
appendLines, the partial-write rollback is gated behind closing the append handle. Ifhandle.close()throws (common on real disk failures — ENOSPC/EIO),rollbackAppendnever runs, partial bytes stay in the log, the retry re-appends the same sequence numbers, and the session becomes permanently unloadable.Evidence
packages/session/session-persistence-jsonl/src/index.ts:651-679:The comment itself states the failure mode: "leaving partial bytes would create duplicate sequence numbers." The rollback (
truncate(before)+ fsync,rollbackAppendat 681-689) is scheduled aftercloseAppendHandle(). When the write/sync fails due to a disk fault,close()frequently fails too (same fault). Then:SessionWriteBehind.startWrite(write-behind.ts:145-152) re-queues the same batch and retries.SessionLogScanner.consumeEventLine(format.ts:364-371) rejects non-contiguous seq in the committed region → wrapped asSessionPersistenceCorruptionError(coordinator.ts:926-929) → the whole session cannot be loaded (verified: a duplicate-seq log is rejected by the scanner).Rollback and close are two independent cleanup actions; one failing must not block the other.
Trigger
Append-time disk error + handle-close failure (low probability, but the consequence is whole-session corruption rather than a lost batch).
Suggested fix
Run
rollbackAppendbefore closing the handle; merge both errors viaAggregateError— the rollback must never be skipped because the close failed. (Note the outerfinallystill closes the handle, so closing early is redundant anyway.)Verified at master
47f9438; duplicate-seq rejection by the scanner was reproduced locally with Node v22.All reactions