@@ -14,6 +14,7 @@ import { tmpdir } from "node:os";
1414import { join } from "node:path" ;
1515import { type } from "arktype" ;
1616
17+ import { createDefaultDependencies } from "@intx/inference/providers" ;
1718import { createInboundMessage } from "@intx/mime" ;
1819import { createIsogitStore } from "@intx/storage-isogit/node" ;
1920import type { AuditRecord , ErrorRecord } from "@intx/types/audit" ;
@@ -59,6 +60,9 @@ function makeRecordingAuditStore(): RecordingAuditStore {
5960 async loadAudit ( _sessionId : string ) : Promise < AuditRecord [ ] > {
6061 return [ ] ;
6162 } ,
63+ async loadErrors ( _sessionId : string ) : Promise < ErrorRecord [ ] > {
64+ return committedErrors . flat ( ) ;
65+ } ,
6266 getCommittedErrors ( ) {
6367 return committedErrors ;
6468 } ,
@@ -91,12 +95,37 @@ function makeFailFirstAuditStore(): FailingAuditStore {
9195 async loadAudit ( _sessionId : string ) : Promise < AuditRecord [ ] > {
9296 return [ ] ;
9397 } ,
98+ async loadErrors ( _sessionId : string ) : Promise < ErrorRecord [ ] > {
99+ return committedErrors . flat ( ) ;
100+ } ,
94101 getCommittedErrors ( ) {
95102 return committedErrors ;
96103 } ,
97104 } ;
98105}
99106
107+ function makeDuplicateErrorAuditStore ( ) : FailingAuditStore {
108+ return {
109+ async commitAudit ( _records : AuditRecord [ ] ) : Promise < void > {
110+ // No-op.
111+ } ,
112+ async commitErrors ( records : ErrorRecord [ ] ) : Promise < void > {
113+ throw new Error (
114+ `Duplicate error record: ${ records [ 0 ] ?. sessionId ?? "session" } /00000000-credential_failure` ,
115+ ) ;
116+ } ,
117+ async loadAudit ( _sessionId : string ) : Promise < AuditRecord [ ] > {
118+ return [ ] ;
119+ } ,
120+ async loadErrors ( _sessionId : string ) : Promise < ErrorRecord [ ] > {
121+ return [ ] ;
122+ } ,
123+ getCommittedErrors ( ) {
124+ return [ ] ;
125+ } ,
126+ } ;
127+ }
128+
100129// Director factory that closes over a caller-supplied `decide` to drive
101130// the reactor through targeted event shapes. The factory shape requires
102131// a configSchema (arktype) and returns a ReactorDirector; this helper
@@ -153,6 +182,86 @@ async function waitForReactorDone(
153182 }
154183}
155184
185+ const FORBIDDEN_DEPS = {
186+ ...createDefaultDependencies ( ) ,
187+ fetch : async ( ) =>
188+ new Response ( "Unauthorized" , { status : 401 , statusText : "Unauthorized" } ) ,
189+ } ;
190+
191+ function credentialFailureDirectors ( ) : BaseEnv [ "directors" ] {
192+ return makeDirectorRegistry (
193+ async (
194+ event : ReactorInboundEvent ,
195+ _state : ReactorState ,
196+ caps : ReactorCapabilities ,
197+ ) => {
198+ if ( event . type === "message.received" ) return caps . infer ( ) ;
199+ if ( event . type === "inference.error" ) {
200+ return [ caps . checkpoint ( "after-error" ) , caps . done ( ) ] ;
201+ }
202+ return caps . done ( ) ;
203+ } ,
204+ ) ;
205+ }
206+
207+ function forbiddenAgentDef ( id : string ) {
208+ return defineAgent ( {
209+ id,
210+ systemPrompt : "test" ,
211+ tools : [ ] ,
212+ capabilities : [ ] ,
213+ inference : {
214+ sources : [
215+ {
216+ provider : UNREACHABLE_SOURCE . provider ,
217+ model : UNREACHABLE_SOURCE . model ,
218+ } ,
219+ ] ,
220+ } ,
221+ } ) ;
222+ }
223+
224+ function duplicateFlushFailures (
225+ events : ReadonlyArray < { type : string ; data ?: unknown } > ,
226+ ) : ReadonlyArray < { type : string ; data ?: unknown } > {
227+ return events . filter ( ( event ) => {
228+ if ( event . type !== "reactor.error" ) return false ;
229+ return JSON . stringify ( event . data ?? { } ) . includes ( "Duplicate error record" ) ;
230+ } ) ;
231+ }
232+
233+ async function runForbiddenCycle ( opts : {
234+ workdir : string ;
235+ sessionId : string ;
236+ agentId : string ;
237+ } ) : Promise < { events : Array < { type : string ; data ?: unknown } > } > {
238+ const store = await createIsogitStore ( opts . workdir ) ;
239+ const env : BaseEnv = {
240+ sources : [ UNREACHABLE_SOURCE ] ,
241+ defaultSource : UNREACHABLE_SOURCE . id ,
242+ storage : store ,
243+ workdir : opts . workdir ,
244+ audit : store ,
245+ authorize : permissiveAuthorize ( ) ,
246+ directors : credentialFailureDirectors ( ) ,
247+ sessionId : opts . sessionId ,
248+ deps : FORBIDDEN_DEPS ,
249+ } ;
250+ const agent = await createAgent ( forbiddenAgentDef ( opts . agentId ) , env ) ;
251+ const events : Array < { type : string ; data ?: unknown } > = [ ] ;
252+ const stream = agent . stream ( ) ;
253+ try {
254+ agent . deliver ( inboundConversation ( ) ) ;
255+ for await ( const event of stream ) {
256+ events . push ( event ) ;
257+ if ( event . type === "reactor.done" ) break ;
258+ }
259+ } finally {
260+ await agent . close ( ) ;
261+ }
262+ return { events } ;
263+ }
264+
156265describe ( "agent error flushing" , ( ) => {
157266 let workDir : string ;
158267
@@ -470,4 +579,101 @@ describe("agent error flushing", () => {
470579 expect ( batches . length ) . toBe ( 1 ) ;
471580 expect ( batches [ 0 ] ?. [ 0 ] ?. source ) . toBe ( "reactor" ) ;
472581 } ) ;
582+
583+ test ( "two credential_failure errors in one session persist without failing the run" , async ( ) => {
584+ const sessionId = "session-credential-once" ;
585+ let inferenceErrors = 0 ;
586+ const store = await createIsogitStore ( workDir ) ;
587+ const env : BaseEnv = {
588+ sources : [ UNREACHABLE_SOURCE ] ,
589+ defaultSource : UNREACHABLE_SOURCE . id ,
590+ storage : store ,
591+ workdir : workDir ,
592+ audit : store ,
593+ authorize : permissiveAuthorize ( ) ,
594+ directors : makeDirectorRegistry (
595+ async (
596+ event : ReactorInboundEvent ,
597+ _state : ReactorState ,
598+ caps : ReactorCapabilities ,
599+ ) => {
600+ if ( event . type === "message.received" ) return caps . infer ( ) ;
601+ if ( event . type === "inference.error" ) {
602+ inferenceErrors += 1 ;
603+ if ( inferenceErrors === 1 ) {
604+ return [ caps . checkpoint ( "after-first" ) , caps . infer ( ) ] ;
605+ }
606+ return [ caps . checkpoint ( "after-second" ) , caps . done ( ) ] ;
607+ }
608+ return caps . done ( ) ;
609+ } ,
610+ ) ,
611+ sessionId,
612+ deps : FORBIDDEN_DEPS ,
613+ } ;
614+ const agent = await createAgent ( forbiddenAgentDef ( "cred-flush-once" ) , env ) ;
615+ const events : Array < { type : string ; data ?: unknown } > = [ ] ;
616+ const stream = agent . stream ( ) ;
617+ try {
618+ agent . deliver ( inboundConversation ( ) ) ;
619+ for await ( const event of stream ) {
620+ events . push ( event ) ;
621+ if ( event . type === "reactor.done" ) break ;
622+ }
623+ } finally {
624+ await agent . close ( ) ;
625+ }
626+
627+ expect ( duplicateFlushFailures ( events ) ) . toEqual ( [ ] ) ;
628+ const records = ( await store . loadErrors ( sessionId ) ) . filter (
629+ ( record ) => record . category === "credential_failure" ,
630+ ) ;
631+ expect ( records ) . toHaveLength ( 2 ) ;
632+ expect ( new Set ( records . map ( ( record ) => record . seq ) ) . size ) . toBe ( 2 ) ;
633+ } ) ;
634+
635+ test ( "two credential_failure errors persist across re-assembly without failing the session" , async ( ) => {
636+ const sessionId = "session-credential" ;
637+ const first = await runForbiddenCycle ( {
638+ workdir : workDir ,
639+ sessionId,
640+ agentId : "cred-flush-1" ,
641+ } ) ;
642+ const second = await runForbiddenCycle ( {
643+ workdir : workDir ,
644+ sessionId,
645+ agentId : "cred-flush-2" ,
646+ } ) ;
647+
648+ expect ( duplicateFlushFailures ( first . events ) ) . toEqual ( [ ] ) ;
649+ expect ( duplicateFlushFailures ( second . events ) ) . toEqual ( [ ] ) ;
650+ const store = await createIsogitStore ( workDir ) ;
651+ const records = ( await store . loadErrors ( sessionId ) ) . filter (
652+ ( record ) => record . category === "credential_failure" ,
653+ ) ;
654+ expect ( records ) . toHaveLength ( 2 ) ;
655+ expect ( new Set ( records . map ( ( record ) => record . seq ) ) . size ) . toBe ( 2 ) ;
656+ } ) ;
657+
658+ test ( "a duplicate error record from commitErrors does not fail the session" , async ( ) => {
659+ const audit = makeDuplicateErrorAuditStore ( ) ;
660+ const directors = credentialFailureDirectors ( ) ;
661+ const def = forbiddenAgentDef ( "cred-flush-duplicate" ) ;
662+ const env = await buildAgentEnv ( { workdir : workDir , audit, directors } ) ;
663+ const agent = await createAgent ( def , { ...env , deps : FORBIDDEN_DEPS } ) ;
664+ const events : Array < { type : string ; data ?: unknown } > = [ ] ;
665+ const stream = agent . stream ( ) ;
666+ try {
667+ agent . deliver ( inboundConversation ( ) ) ;
668+ for await ( const event of stream ) {
669+ events . push ( event ) ;
670+ if ( event . type === "reactor.done" ) break ;
671+ }
672+ } finally {
673+ await agent . close ( ) ;
674+ }
675+
676+ expect ( duplicateFlushFailures ( events ) ) . toEqual ( [ ] ) ;
677+ expect ( events . some ( ( event ) => event . type === "reactor.done" ) ) . toBe ( true ) ;
678+ } ) ;
473679} ) ;
0 commit comments