@@ -126,6 +126,44 @@ function makeDuplicateErrorAuditStore(): FailingAuditStore {
126126 } ;
127127}
128128
129+ // Audit store that collides on the first batch's leading record only,
130+ // simulating a stale-seq assembly flushing [seq0/dup, seq1/fresh]: the
131+ // first `commitErrors` throws `Duplicate error record` naming the
132+ // colliding record's file key, and the retry succeeds.
133+ function makePartialDuplicateAuditStore ( ) : FailingAuditStore {
134+ const committedErrors : ErrorRecord [ ] [ ] = [ ] ;
135+ let firstAttempt = true ;
136+ return {
137+ async commitAudit ( _records : AuditRecord [ ] ) : Promise < void > {
138+ // No-op.
139+ } ,
140+ async commitErrors ( records : ErrorRecord [ ] ) : Promise < void > {
141+ if ( firstAttempt ) {
142+ firstAttempt = false ;
143+ const colliding = records [ 0 ] ;
144+ const seq = String ( colliding ?. seq ?? 0 ) . padStart ( 8 , "0" ) ;
145+ const category = ( colliding ?. category ?? "" ) . replace (
146+ / [ ^ a - z A - Z 0 - 9 _ - ] / g,
147+ "_" ,
148+ ) ;
149+ throw new Error (
150+ `Duplicate error record: ${ colliding ?. sessionId ?? "session" } /${ seq } -${ category } ` ,
151+ ) ;
152+ }
153+ committedErrors . push ( [ ...records ] ) ;
154+ } ,
155+ async loadAudit ( _sessionId : string ) : Promise < AuditRecord [ ] > {
156+ return [ ] ;
157+ } ,
158+ async loadErrors ( _sessionId : string ) : Promise < ErrorRecord [ ] > {
159+ return [ ] ;
160+ } ,
161+ getCommittedErrors ( ) {
162+ return committedErrors ;
163+ } ,
164+ } ;
165+ }
166+
129167// Director factory that closes over a caller-supplied `decide` to drive
130168// the reactor through targeted event shapes. The factory shape requires
131169// a configSchema (arktype) and returns a ReactorDirector; this helper
@@ -677,6 +715,49 @@ describe("agent error flushing", () => {
677715 expect ( events . some ( ( event ) => event . type === "reactor.done" ) ) . toBe ( true ) ;
678716 } ) ;
679717
718+ test ( "a partial duplicate collision drops only the colliding record" , async ( ) => {
719+ // A stale-seq assembly flushing [seq0/dup, seq1/fresh] must persist
720+ // the fresh record: the first commit names only the colliding key,
721+ // so the flush drops that record and retries the rest.
722+ const audit = makePartialDuplicateAuditStore ( ) ;
723+ let inferenceErrors = 0 ;
724+ const directors = makeDirectorRegistry (
725+ async (
726+ event : ReactorInboundEvent ,
727+ _state : ReactorState ,
728+ caps : ReactorCapabilities ,
729+ ) => {
730+ if ( event . type === "message.received" ) return caps . infer ( ) ;
731+ if ( event . type === "inference.error" ) {
732+ inferenceErrors += 1 ;
733+ if ( inferenceErrors === 1 ) return caps . infer ( ) ;
734+ return [ caps . checkpoint ( "after-second" ) , caps . done ( ) ] ;
735+ }
736+ return caps . done ( ) ;
737+ } ,
738+ ) ;
739+ const def = forbiddenAgentDef ( "cred-flush-partial-duplicate" ) ;
740+ const env = await buildAgentEnv ( { workdir : workDir , audit, directors } ) ;
741+ const agent = await createAgent ( def , { ...env , deps : FORBIDDEN_DEPS } ) ;
742+ const events : Array < { type : string ; data ?: unknown } > = [ ] ;
743+ const stream = agent . stream ( ) ;
744+ try {
745+ agent . deliver ( inboundConversation ( ) ) ;
746+ for await ( const event of stream ) {
747+ events . push ( event ) ;
748+ if ( event . type === "reactor.done" ) break ;
749+ }
750+ } finally {
751+ await agent . close ( ) ;
752+ }
753+
754+ expect ( duplicateFlushFailures ( events ) ) . toEqual ( [ ] ) ;
755+ expect ( events . some ( ( event ) => event . type === "reactor.done" ) ) . toBe ( true ) ;
756+ const persisted = audit . getCommittedErrors ( ) . flat ( ) ;
757+ expect ( persisted ) . toHaveLength ( 1 ) ;
758+ expect ( persisted [ 0 ] ?. seq ) . toBe ( 1 ) ;
759+ } ) ;
760+
680761 test ( "createAgent still assembles when loadErrors throws" , async ( ) => {
681762 const audit = makeRecordingAuditStore ( ) ;
682763 audit . loadErrors = async ( ) => {
0 commit comments