@@ -1169,6 +1169,82 @@ describe("ProviderCommandReactor", () => {
11691169 } ) ,
11701170 ) ;
11711171
1172+ effectIt . effect (
1173+ "preserves a pending admission when runtime mode changes during provider start" ,
1174+ ( ) =>
1175+ Effect . gen ( function * ( ) {
1176+ const startEntered = yield * Deferred . make < void > ( ) ;
1177+ const releaseStart = yield * Deferred . make < void > ( ) ;
1178+ const harness = yield * Effect . promise ( ( ) =>
1179+ createHarness ( {
1180+ startSessionEffect : ( session ) =>
1181+ Deferred . succeed ( startEntered , undefined ) . pipe (
1182+ Effect . andThen ( Deferred . await ( releaseStart ) ) ,
1183+ Effect . as ( session ) ,
1184+ ) ,
1185+ } ) ,
1186+ ) ;
1187+ const requestId = CommandId . make ( "cmd-turn-start-runtime-mode-race" ) ;
1188+ const messageId = asMessageId ( "user-message-runtime-mode-race" ) ;
1189+
1190+ yield * harness . engine . dispatch ( {
1191+ type : "thread.turn.start" ,
1192+ commandId : requestId ,
1193+ threadId : ThreadId . make ( "thread-1" ) ,
1194+ message : {
1195+ messageId,
1196+ role : "user" ,
1197+ text : "keep this admission" ,
1198+ attachments : [ ] ,
1199+ } ,
1200+ interactionMode : DEFAULT_PROVIDER_INTERACTION_MODE ,
1201+ runtimeMode : "approval-required" ,
1202+ createdAt : "2026-01-01T00:00:00.000Z" ,
1203+ } ) ;
1204+ yield * Deferred . await ( startEntered ) ;
1205+
1206+ yield * harness . engine . dispatch ( {
1207+ type : "thread.runtime-mode.set" ,
1208+ commandId : CommandId . make ( "cmd-runtime-mode-set-during-admission" ) ,
1209+ threadId : ThreadId . make ( "thread-1" ) ,
1210+ runtimeMode : "full-access" ,
1211+ createdAt : "2026-01-01T00:00:01.000Z" ,
1212+ } ) ;
1213+ yield * Effect . promise ( ( ) => harness . drain ( ) ) ;
1214+
1215+ let readModel = yield * Effect . promise ( ( ) => harness . readModel ( ) ) ;
1216+ let thread = readModel . threads . find ( ( entry ) => entry . id === ThreadId . make ( "thread-1" ) ) ;
1217+ expect ( harness . startSession ) . toHaveBeenCalledTimes ( 1 ) ;
1218+ expect ( thread ?. session ?. status ) . toBe ( "starting" ) ;
1219+ expect ( thread ?. session ?. pendingTurnRequestId ) . toBe ( requestId ) ;
1220+ expect ( thread ?. session ?. pendingTurnMessageId ) . toBe ( messageId ) ;
1221+
1222+ const subscriptionReady = yield * Deferred . make < void > ( ) ;
1223+ const runningSession = yield * Stream . runHead (
1224+ harness . engine . streamDomainEvents . pipe (
1225+ Stream . onStart ( Deferred . succeed ( subscriptionReady , undefined ) ) ,
1226+ Stream . filter (
1227+ ( event ) =>
1228+ event . type === "thread.session-set" &&
1229+ event . payload . threadId === ThreadId . make ( "thread-1" ) &&
1230+ event . payload . session . status === "running" &&
1231+ event . payload . session . activeTurnRequestId === requestId ,
1232+ ) ,
1233+ ) ,
1234+ ) . pipe ( Effect . forkChild ) ;
1235+ yield * Deferred . await ( subscriptionReady ) ;
1236+ yield * Deferred . succeed ( releaseStart , undefined ) ;
1237+ yield * Fiber . join ( runningSession ) ;
1238+
1239+ readModel = yield * Effect . promise ( ( ) => harness . readModel ( ) ) ;
1240+ thread = readModel . threads . find ( ( entry ) => entry . id === ThreadId . make ( "thread-1" ) ) ;
1241+ expect ( thread ?. runtimeMode ) . toBe ( "full-access" ) ;
1242+ expect ( thread ?. session ?. status ) . toBe ( "running" ) ;
1243+ expect ( thread ?. session ?. activeTurnRequestId ) . toBe ( requestId ) ;
1244+ expect ( harness . startSession ) . toHaveBeenCalledTimes ( 1 ) ;
1245+ } ) ,
1246+ ) ;
1247+
11721248 effectIt . effect ( "interrupts a detached admission when the reactor layer closes" , ( ) =>
11731249 Effect . gen ( function * ( ) {
11741250 const providerStartInterrupted = yield * Deferred . make < void > ( ) ;
@@ -1331,11 +1407,12 @@ describe("ProviderCommandReactor", () => {
13311407 effectIt . effect ( "retries unknown per-instance inventory then records an inventory error" , ( ) =>
13321408 Effect . gen ( function * ( ) {
13331409 const testClock = yield * TestClock . make ( ) ;
1334- yield * testClock . setTime ( PROVIDER_TURN_ADMISSION_TIMEOUT_MS + 1 ) ;
1410+ yield * testClock . setTime ( 0 ) ;
13351411 const requestId = CommandId . make ( "cmd-boot-inventory-unknown" ) ;
13361412 const harness = yield * Effect . promise ( ( ) =>
13371413 createHarness ( {
13381414 clock : testClock ,
1415+ beforeReactorStart : testClock . adjust ( PROVIDER_TURN_ADMISSION_TIMEOUT_MS + 1 ) ,
13391416 overdueTurnStartBeforeReactor : {
13401417 commandId : requestId ,
13411418 messageId : asMessageId ( "message-boot-inventory-unknown" ) ,
@@ -1363,15 +1440,119 @@ describe("ProviderCommandReactor", () => {
13631440 } ) ,
13641441 ) ;
13651442
1443+ effectIt . effect ( "preserves a non-overdue admission when boot inventory is unknown" , ( ) =>
1444+ Effect . gen ( function * ( ) {
1445+ const testClock = yield * TestClock . make ( ) ;
1446+ yield * testClock . setTime ( 0 ) ;
1447+ const requestId = CommandId . make ( "cmd-boot-inventory-unknown-not-overdue" ) ;
1448+ const harness = yield * Effect . promise ( ( ) =>
1449+ createHarness ( {
1450+ clock : testClock ,
1451+ overdueTurnStartBeforeReactor : {
1452+ commandId : requestId ,
1453+ messageId : asMessageId ( "message-boot-inventory-unknown-not-overdue" ) ,
1454+ createdAt : isoAt ( 0 ) ,
1455+ sessionIncarnationId : RuntimeSessionId . make (
1456+ "session-boot-inventory-unknown-not-overdue" ,
1457+ ) ,
1458+ } ,
1459+ inventoryEffect : ( ) =>
1460+ Effect . fail (
1461+ new ProviderAdapterRequestError ( {
1462+ provider : "codex" ,
1463+ method : "listSessions" ,
1464+ detail : "inventory temporarily unavailable" ,
1465+ } ) ,
1466+ ) ,
1467+ } ) ,
1468+ ) ;
1469+
1470+ let readModel = yield * Effect . promise ( ( ) => harness . readModel ( ) ) ;
1471+ let thread = readModel . threads . find ( ( entry ) => entry . id === ThreadId . make ( "thread-1" ) ) ;
1472+ expect ( thread ?. session ?. status ) . toBe ( "starting" ) ;
1473+ expect ( thread ?. session ?. pendingTurnRequestId ) . toBe ( requestId ) ;
1474+ expect ( harness . listSessionsForInstance ) . toHaveBeenCalledTimes ( 3 ) ;
1475+
1476+ yield * testClock . adjust ( PROVIDER_TURN_ADMISSION_TIMEOUT_MS - 1 ) ;
1477+ readModel = yield * Effect . promise ( ( ) => harness . readModel ( ) ) ;
1478+ thread = readModel . threads . find ( ( entry ) => entry . id === ThreadId . make ( "thread-1" ) ) ;
1479+ expect ( thread ?. session ?. status ) . toBe ( "starting" ) ;
1480+
1481+ yield * testClock . adjust ( 1 ) ;
1482+ readModel = yield * Effect . promise ( ( ) => harness . readModel ( ) ) ;
1483+ thread = readModel . threads . find ( ( entry ) => entry . id === ThreadId . make ( "thread-1" ) ) ;
1484+ expect ( thread ?. session ?. status ) . toBe ( "error" ) ;
1485+ expect ( thread ?. session ?. failedTurnRequestId ) . toBe ( requestId ) ;
1486+ expect ( thread ?. session ?. lastError ) . toContain ( "could not inventory" ) ;
1487+ expect ( thread ?. session ?. lastError ) . toContain ( "inventory temporarily unavailable" ) ;
1488+ } ) ,
1489+ ) ;
1490+
1491+ effectIt . effect ( "keeps an exact late start after boot inventory was unknown" , ( ) =>
1492+ Effect . gen ( function * ( ) {
1493+ const testClock = yield * TestClock . make ( ) ;
1494+ yield * testClock . setTime ( 0 ) ;
1495+ const requestId = CommandId . make ( "cmd-boot-inventory-unknown-late-start" ) ;
1496+ const messageId = asMessageId ( "message-boot-inventory-unknown-late-start" ) ;
1497+ const sessionIncarnationId = RuntimeSessionId . make (
1498+ "session-boot-inventory-unknown-late-start" ,
1499+ ) ;
1500+ const harness = yield * Effect . promise ( ( ) =>
1501+ createHarness ( {
1502+ clock : testClock ,
1503+ overdueTurnStartBeforeReactor : {
1504+ commandId : requestId ,
1505+ messageId,
1506+ createdAt : isoAt ( 0 ) ,
1507+ sessionIncarnationId,
1508+ } ,
1509+ inventoryEffect : ( ) =>
1510+ Effect . fail (
1511+ new ProviderAdapterRequestError ( {
1512+ provider : "codex" ,
1513+ method : "listSessions" ,
1514+ detail : "inventory temporarily unavailable" ,
1515+ } ) ,
1516+ ) ,
1517+ } ) ,
1518+ ) ;
1519+
1520+ yield * harness . engine . dispatch ( {
1521+ type : "thread.turn.admission.accept" ,
1522+ commandId : CommandId . make ( "cmd-boot-inventory-unknown-late-start-accept" ) ,
1523+ threadId : ThreadId . make ( "thread-1" ) ,
1524+ requestId,
1525+ messageId,
1526+ providerInstanceId : ProviderInstanceId . make ( "codex" ) ,
1527+ sessionIncarnationId,
1528+ turnId : asTurnId ( "turn-boot-inventory-unknown-late-start" ) ,
1529+ createdAt : isoAt ( 1 ) ,
1530+ } ) ;
1531+ yield * testClock . adjust ( PROVIDER_TURN_ADMISSION_TIMEOUT_MS ) ;
1532+
1533+ const readModel = yield * Effect . promise ( ( ) => harness . readModel ( ) ) ;
1534+ const thread = readModel . threads . find ( ( entry ) => entry . id === ThreadId . make ( "thread-1" ) ) ;
1535+ expect ( thread ?. session ?. status ) . toBe ( "running" ) ;
1536+ expect ( thread ?. session ?. activeTurnRequestId ) . toBe ( requestId ) ;
1537+ expect ( thread ?. session ?. activeTurnId ) . toBe (
1538+ asTurnId ( "turn-boot-inventory-unknown-late-start" ) ,
1539+ ) ;
1540+ expect (
1541+ thread ?. activities . filter ( ( activity ) => activity . kind === "provider.turn.start.failed" ) ,
1542+ ) . toHaveLength ( 0 ) ;
1543+ } ) ,
1544+ ) ;
1545+
13661546 effectIt . effect ( "bounds a hanging per-instance inventory retry chain" , ( ) =>
13671547 Effect . gen ( function * ( ) {
13681548 const testClock = yield * TestClock . make ( ) ;
1369- yield * testClock . setTime ( PROVIDER_TURN_ADMISSION_TIMEOUT_MS + 1 ) ;
1549+ yield * testClock . setTime ( 0 ) ;
13701550 const requestId = CommandId . make ( "cmd-boot-inventory-hangs" ) ;
13711551 const inventoryEntered = yield * Deferred . make < void > ( ) ;
13721552 const harnessFiber = yield * Effect . promise ( ( ) =>
13731553 createHarness ( {
13741554 clock : testClock ,
1555+ beforeReactorStart : testClock . adjust ( PROVIDER_TURN_ADMISSION_TIMEOUT_MS + 1 ) ,
13751556 overdueTurnStartBeforeReactor : {
13761557 commandId : requestId ,
13771558 messageId : asMessageId ( "message-boot-inventory-hangs" ) ,
0 commit comments